@adecore/editor-react 0.22.1 → 0.23.1

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 (209) hide show
  1. package/dist/CallHierarchyPanel.d.ts +6 -0
  2. package/dist/CallHierarchyPanel.js +92 -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/PanelResizeHandle.d.ts +8 -0
  15. package/dist/PanelResizeHandle.js +71 -0
  16. package/dist/PeekPanel.d.ts +3 -0
  17. package/dist/PeekPanel.js +7 -4
  18. package/dist/PickPopup.js +15 -2
  19. package/dist/RenameCard.js +10 -5
  20. package/dist/RenderedDocs.d.ts +9 -0
  21. package/dist/RenderedDocs.js +84 -0
  22. package/dist/SplitDiffView.d.ts +9 -0
  23. package/dist/SplitDiffView.js +276 -0
  24. package/dist/SymbolPicker.d.ts +3 -0
  25. package/dist/SymbolPicker.js +4 -3
  26. package/dist/call-hierarchy-model.d.ts +52 -0
  27. package/dist/call-hierarchy-model.js +93 -0
  28. package/dist/call-hierarchy.d.ts +42 -0
  29. package/dist/call-hierarchy.js +398 -0
  30. package/dist/code-actions-model.d.ts +2 -0
  31. package/dist/code-actions-model.js +12 -10
  32. package/dist/code-actions.js +3 -1
  33. package/dist/completion-model.d.ts +14 -4
  34. package/dist/completion-model.js +40 -33
  35. package/dist/completion.d.ts +4 -0
  36. package/dist/completion.js +257 -42
  37. package/dist/diagnostics.js +8 -6
  38. package/dist/diff-expansion.d.ts +9 -0
  39. package/dist/diff-expansion.js +41 -0
  40. package/dist/diff-overlays.d.ts +15 -0
  41. package/dist/diff-overlays.js +28 -0
  42. package/dist/diff-syntax.d.ts +16 -0
  43. package/dist/diff-syntax.js +49 -0
  44. package/dist/diff-wordmarks.d.ts +5 -0
  45. package/dist/diff-wordmarks.js +26 -0
  46. package/dist/doc-comment-model.d.ts +23 -0
  47. package/dist/doc-comment-model.js +291 -0
  48. package/dist/editor-language.d.ts +13 -1
  49. package/dist/editor-language.js +15 -5
  50. package/dist/editor-react.css +79 -0
  51. package/dist/format-on-type.d.ts +14 -0
  52. package/dist/format-on-type.js +96 -0
  53. package/dist/host-types.d.ts +19 -0
  54. package/dist/hover.d.ts +4 -0
  55. package/dist/hover.js +15 -2
  56. package/dist/index.d.ts +19 -5
  57. package/dist/index.js +9 -1
  58. package/dist/inlay-hints.d.ts +47 -2
  59. package/dist/inlay-hints.js +353 -16
  60. package/dist/language-ids.js +4 -2
  61. package/dist/locales/en.json +117 -3
  62. package/dist/locales/nl.json +117 -3
  63. package/dist/models.d.ts +6 -2
  64. package/dist/models.js +5 -1
  65. package/dist/panel-size.d.ts +6 -0
  66. package/dist/panel-size.js +25 -0
  67. package/dist/patch-decorations.d.ts +27 -0
  68. package/dist/patch-decorations.js +182 -0
  69. package/dist/patch-layout.d.ts +40 -0
  70. package/dist/patch-layout.js +304 -0
  71. package/dist/peek.d.ts +3 -1
  72. package/dist/peek.js +6 -1
  73. package/dist/pick.d.ts +1 -0
  74. package/dist/pick.js +11 -1
  75. package/dist/popups.d.ts +17 -0
  76. package/dist/popups.js +1 -0
  77. package/dist/project-language.d.ts +12 -0
  78. package/dist/project-language.js +52 -2
  79. package/dist/refresher.d.ts +1 -0
  80. package/dist/refresher.js +5 -0
  81. package/dist/rename-model.d.ts +1 -1
  82. package/dist/rename-model.js +3 -7
  83. package/dist/rename.js +5 -8
  84. package/dist/rendered-docs.d.ts +64 -0
  85. package/dist/rendered-docs.js +374 -0
  86. package/dist/row-host.d.ts +2 -0
  87. package/dist/row-host.js +11 -3
  88. package/dist/semantic-delta.d.ts +2 -0
  89. package/dist/semantic-delta.js +27 -0
  90. package/dist/semantic-model.js +16 -0
  91. package/dist/semantic-tokens.d.ts +26 -1
  92. package/dist/semantic-tokens.js +174 -14
  93. package/dist/shortcut-keys.d.ts +2 -0
  94. package/dist/shortcut-keys.js +6 -2
  95. package/dist/signature.js +1 -1
  96. package/dist/snippet-field-transforms.d.ts +30 -0
  97. package/dist/snippet-field-transforms.js +116 -0
  98. package/dist/snippet-indent.d.ts +2 -0
  99. package/dist/snippet-indent.js +49 -0
  100. package/dist/snippet-range-order.d.ts +3 -0
  101. package/dist/snippet-range-order.js +19 -0
  102. package/dist/snippet-session.d.ts +42 -6
  103. package/dist/snippet-session.js +650 -53
  104. package/dist/snippet-transform-client.d.ts +19 -0
  105. package/dist/snippet-transform-client.js +66 -0
  106. package/dist/snippet-transform-format.d.ts +15 -0
  107. package/dist/snippet-transform-format.js +302 -0
  108. package/dist/snippet-transform-protocol.d.ts +45 -0
  109. package/dist/snippet-transform-protocol.js +8 -0
  110. package/dist/snippet-transform-queue.d.ts +32 -0
  111. package/dist/snippet-transform-queue.js +173 -0
  112. package/dist/snippet-transform-worker.d.ts +10 -0
  113. package/dist/snippet-transform-worker.js +41 -0
  114. package/dist/snippet-variable-names.d.ts +1 -0
  115. package/dist/snippet-variable-names.js +36 -0
  116. package/dist/snippet-variables.d.ts +7 -0
  117. package/dist/snippet-variables.js +66 -0
  118. package/dist/snippet.d.ts +30 -3
  119. package/dist/snippet.js +402 -87
  120. package/dist/split-alignment.d.ts +6 -0
  121. package/dist/split-alignment.js +38 -0
  122. package/dist/split-scroll.d.ts +14 -0
  123. package/dist/split-scroll.js +45 -0
  124. package/dist/symbols.d.ts +2 -1
  125. package/dist/symbols.js +39 -7
  126. package/dist/testing.d.ts +5 -1
  127. package/dist/testing.js +12 -0
  128. package/dist/timers.d.ts +1 -0
  129. package/dist/timers.js +4 -0
  130. package/dist/use-panel-body.d.ts +6 -0
  131. package/dist/use-panel-body.js +13 -0
  132. package/dist/workspace-edit.d.ts +25 -3
  133. package/dist/workspace-edit.js +282 -143
  134. package/package.json +10 -5
  135. package/src/CallHierarchyPanel.tsx +244 -0
  136. package/src/CompletionPopup.tsx +12 -1
  137. package/src/DiffView.tsx +262 -0
  138. package/src/EditorContextMenu.tsx +1 -0
  139. package/src/EditorView.tsx +21 -2
  140. package/src/FindReplace.tsx +51 -13
  141. package/src/HoverCard.tsx +23 -21
  142. package/src/LanguagePopups.tsx +5 -0
  143. package/src/Markdown.tsx +10 -3
  144. package/src/PanelResizeHandle.tsx +104 -0
  145. package/src/PeekPanel.tsx +11 -6
  146. package/src/PickPopup.tsx +18 -2
  147. package/src/RenameCard.tsx +18 -0
  148. package/src/RenderedDocs.tsx +155 -0
  149. package/src/SplitDiffView.tsx +337 -0
  150. package/src/SymbolPicker.tsx +4 -3
  151. package/src/call-hierarchy-model.ts +151 -0
  152. package/src/call-hierarchy.ts +440 -0
  153. package/src/code-actions-model.ts +17 -11
  154. package/src/code-actions.ts +3 -1
  155. package/src/completion-model.ts +61 -35
  156. package/src/completion.ts +302 -42
  157. package/src/diagnostics.ts +8 -6
  158. package/src/diff-expansion.ts +49 -0
  159. package/src/diff-overlays.tsx +69 -0
  160. package/src/diff-syntax.ts +67 -0
  161. package/src/diff-wordmarks.ts +31 -0
  162. package/src/doc-comment-model.ts +336 -0
  163. package/src/editor-language.ts +22 -6
  164. package/src/editor-react.css +79 -0
  165. package/src/format-on-type.ts +111 -0
  166. package/src/host-types.ts +23 -0
  167. package/src/hover.ts +18 -3
  168. package/src/index.ts +27 -4
  169. package/src/inlay-hints.ts +394 -18
  170. package/src/language-ids.ts +4 -2
  171. package/src/locales/en.json +117 -3
  172. package/src/locales/nl.json +117 -3
  173. package/src/models.ts +40 -1
  174. package/src/panel-size.ts +28 -0
  175. package/src/patch-decorations.ts +224 -0
  176. package/src/patch-layout.ts +388 -0
  177. package/src/peek.ts +6 -1
  178. package/src/pick.ts +12 -1
  179. package/src/popups.ts +24 -0
  180. package/src/project-language.ts +63 -2
  181. package/src/refresher.ts +6 -0
  182. package/src/rename-model.ts +4 -8
  183. package/src/rename.ts +5 -8
  184. package/src/rendered-docs.ts +430 -0
  185. package/src/row-host.ts +12 -3
  186. package/src/semantic-delta.ts +29 -0
  187. package/src/semantic-model.ts +17 -0
  188. package/src/semantic-tokens.ts +202 -19
  189. package/src/shortcut-keys.ts +6 -2
  190. package/src/signature.ts +1 -1
  191. package/src/snippet-field-transforms.ts +144 -0
  192. package/src/snippet-indent.ts +53 -0
  193. package/src/snippet-range-order.ts +19 -0
  194. package/src/snippet-session.ts +797 -56
  195. package/src/snippet-transform-client.ts +84 -0
  196. package/src/snippet-transform-format.ts +325 -0
  197. package/src/snippet-transform-protocol.ts +24 -0
  198. package/src/snippet-transform-queue.ts +199 -0
  199. package/src/snippet-transform-worker.ts +58 -0
  200. package/src/snippet-variable-names.ts +36 -0
  201. package/src/snippet-variables.ts +74 -0
  202. package/src/snippet.ts +452 -89
  203. package/src/split-alignment.ts +45 -0
  204. package/src/split-scroll.ts +54 -0
  205. package/src/symbols.ts +43 -7
  206. package/src/testing.ts +25 -0
  207. package/src/timers.ts +5 -0
  208. package/src/use-panel-body.ts +17 -0
  209. package/src/workspace-edit.ts +347 -169
@@ -1,185 +1,324 @@
1
- import { applyTextEdits, fileUriToPath } from '@adecore/lsp';
1
+ import { fileUriToPath, planWorkspaceOperations } from '@adecore/lsp';
2
2
  import { messageOf } from './error-message.js';
3
3
  function refused(failureReason) {
4
4
  return { applied: false, failureReason };
5
5
  }
6
- /* The steps of an edit; null when it deletes a file, which an edit is not applied with. */
7
- function stepsOf(edit) {
8
- const steps = [];
9
- for (const change of edit.documentChanges ?? []) {
10
- if ('textDocument' in change) {
11
- steps.push({ kind: 'edits', uri: change.textDocument.uri, edits: change.edits });
12
- }
13
- else if (change.kind === 'create') {
14
- steps.push({ kind: 'create', file: change });
15
- }
16
- else if (change.kind === 'rename') {
17
- steps.push({ kind: 'rename', oldUri: change.oldUri, newUri: change.newUri });
18
- }
19
- else {
20
- return null;
21
- }
22
- }
6
+ /* The text edits of an edit per document, as the entries a document has in it, whatever else the edit does. */
7
+ export function textEntriesOf(edit) {
8
+ const entries = new Map();
9
+ const add = (uri, edits) => {
10
+ entries.set(uri, [...(entries.get(uri) ?? []), edits]);
11
+ };
23
12
  if (edit.documentChanges === undefined) {
24
13
  for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
25
- steps.push({ kind: 'edits', uri, edits });
14
+ add(uri, edits);
26
15
  }
16
+ return entries;
27
17
  }
28
- return steps;
29
- }
30
- /* The text edits of an edit per document, as the entries a document has in it; null when it deletes a file. */
31
- export function entriesOf(edit) {
32
- const steps = stepsOf(edit);
33
- if (steps === null) {
34
- return null;
35
- }
36
- const entries = new Map();
37
- for (const step of steps) {
38
- if (step.kind === 'edits') {
39
- entries.set(step.uri, [...(entries.get(step.uri) ?? []), step.edits]);
18
+ for (const change of edit.documentChanges) {
19
+ if ('textDocument' in change) {
20
+ add(change.textDocument.uri, change.edits);
40
21
  }
41
22
  }
42
23
  return entries;
43
24
  }
25
+ /* The text edits of an edit per document, as the entries a document has in it; null when it deletes a file. */
26
+ export function entriesOf(edit) {
27
+ return (edit.documentChanges ?? []).some((change) => 'kind' in change && change.kind === 'delete') ? null : textEntriesOf(edit);
28
+ }
44
29
  /* The files an edit moves, which a preview of lines cannot show. */
45
30
  export function renamesOf(edit) {
46
31
  return (edit.documentChanges ?? []).filter((change) => 'kind' in change && change.kind === 'rename');
47
32
  }
33
+ /* Every file an edit makes, moves or deletes, in the order it says. */
34
+ export function resourceOperationsOf(edit) {
35
+ const operations = [];
36
+ for (const change of edit.documentChanges ?? []) {
37
+ if ('textDocument' in change) {
38
+ continue;
39
+ }
40
+ operations.push(change.kind === 'rename' ? { kind: 'rename', uri: change.oldUri, newUri: change.newUri } : { kind: change.kind, uri: change.uri });
41
+ }
42
+ return operations;
43
+ }
48
44
  function contentChangeOf(edit) {
49
45
  return { range: edit.range, text: edit.newText };
50
46
  }
51
- /*
52
- * Applies an LSP workspace edit step by step, after checking it against every text so one that does not fit
53
- * changes nothing. A file an editor holds takes its edits as one undo step; any other file gets an unsaved
54
- * draft, since nothing is written to disk on a server's word alone, unless the edit also moves a file: then it
55
- * is one change, saved and moved through the machine. A file the edit creates is made through the machine.
56
- */
57
- export async function applyWorkspaceEdit(edit, host) {
58
- const steps = stepsOf(edit);
59
- if (steps === null) {
60
- return refused('Deleting files is not supported yet');
61
- }
62
- const planned = await plan(steps, host);
63
- if (typeof planned === 'string') {
64
- return refused(planned);
65
- }
66
- const moving = planned.some((item) => item.kind === 'rename');
67
- const staged = [];
68
- for (const item of planned) {
69
- if (item.kind === 'rename' || item.kind === 'create') {
70
- const reason = item.kind === 'rename' ? await host.files.rename(item.oldPath, item.newPath) : await host.files.create(item.path, item.text);
71
- if (reason !== null) {
72
- return refused(reason);
47
+ /* The resource operations a project has a way to carry out; pass it as the session's `resourceOperations` so servers only ask for what can be done. */
48
+ export function supportedResourceOperations(files) {
49
+ const operations = [];
50
+ if (files?.create !== undefined) {
51
+ operations.push('create');
52
+ }
53
+ if (files?.rename !== undefined) {
54
+ operations.push('rename');
55
+ }
56
+ if (files?.delete !== undefined) {
57
+ operations.push('delete');
58
+ }
59
+ return operations;
60
+ }
61
+ function within(uri, folder) {
62
+ return uri.startsWith(`${folder}/`);
63
+ }
64
+ /* Where a deleted URI stood before the renames that ran ahead of it, and what was moved into it: the places unsaved work can be. */
65
+ function placesOf(uri, renames) {
66
+ const found = new Set([uri]);
67
+ for (let i = renames.length - 1; i >= 0; i--) {
68
+ const { oldUri, newUri } = renames[i];
69
+ for (const place of [...found]) {
70
+ if (place === newUri || within(place, newUri)) {
71
+ found.add(oldUri + place.slice(newUri.length));
73
72
  }
74
- continue;
75
- }
76
- if (item.after === item.before) {
77
- continue;
78
- }
79
- const editor = host.editorOf(item.uri);
80
- if (editor) {
81
- // The edits stay apart, so the caret and what is drawn over the text stay where they were between them.
82
- if (!editor.applyEdits(item.edits.map(contentChangeOf))) {
83
- return refused(`${item.uri} is read only`);
73
+ else if (within(newUri, place)) {
74
+ found.add(oldUri);
84
75
  }
85
- continue;
86
- }
87
- const path = fileUriToPath(item.uri);
88
- if (path === null || item.disk === null) {
89
- continue;
90
- }
91
- const file = { path, disk: item.disk, text: item.after };
92
- if (!moving) {
93
- staged.push(file);
94
- continue;
95
- }
96
- const reason = await host.files.save([file]);
97
- if (reason !== null) {
98
- return refused(reason);
99
76
  }
100
77
  }
101
- if (staged.length > 0) {
102
- host.files?.stage(staged);
78
+ return [...found];
79
+ }
80
+ /* Whether deleting or replacing loses work nobody saved at any of the places. Without `isUnsaved` that is an editor on, or under, a place. A `recursive` one may be a folder. */
81
+ function losesWork(recursive, places, host) {
82
+ const files = host.files;
83
+ if (files.isUnsaved !== undefined) {
84
+ return places.some((place) => {
85
+ const path = fileUriToPath(place);
86
+ return path !== null && files.isUnsaved(path);
87
+ });
103
88
  }
104
- return { applied: true };
89
+ if (host.openUris === undefined && recursive) {
90
+ return true;
91
+ }
92
+ const open = host.openUris?.() ?? [];
93
+ return places.some((place) => host.editorOf(place) !== undefined || open.some((uri) => uri === place || within(uri, place)));
105
94
  }
106
- /* Reads every text the edit needs and works out what each step leaves, or says why it does not fit. */
107
- async function plan(steps, host) {
108
- const texts = new Map();
109
- // A file that has moved is read where it still stands: nothing moves until the plan is made.
110
- const origins = new Map();
111
- // A file that is made is written once, with what every step leaves in it.
112
- const creates = new Map();
113
- const planned = [];
95
+ function readOnlyAt(host, uri, editor) {
96
+ return host.isReadOnly?.(uri) ?? editor?.isReadOnly?.() ?? false;
97
+ }
98
+ /* Why a create or move over a file that is there is refused; null when the host can replace it without losing work. */
99
+ function overwriteProblem(step, target, places, host, reads) {
100
+ const path = fileUriToPath(target);
101
+ if (host.files?.supportsOverwrite !== true) {
102
+ return `${path} already exists`;
103
+ }
104
+ // A target no text was read from may be a folder, with unsaved work anywhere under it.
105
+ const isText = step.kind === 'create' || reads.open.has(target) || reads.disks.has(target);
106
+ if (losesWork(!isText, places, host)) {
107
+ return `${path} has changes that are not saved`;
108
+ }
109
+ const editor = host.editorOf(target);
110
+ return editor !== undefined && step.kind === 'create' && readOnlyAt(host, target, editor) ? `${path} is read only` : null;
111
+ }
112
+ /* Checks a plan against what only the host can see: paths, overwrites, unsaved work and the editors it touches. Null when it fits. */
113
+ function problemWith(steps, host, reads) {
114
+ const renames = [];
114
115
  for (const step of steps) {
115
- if (step.kind === 'create') {
116
- const made = await planCreate(step.file, host, texts);
117
- if (typeof made === 'string') {
118
- return made;
119
- }
120
- if (made !== null) {
121
- creates.set(step.file.uri, made);
122
- planned.push(made);
116
+ if (step.kind === 'edits') {
117
+ if (step.open && readOnlyAt(host, step.readUri, host.editorOf(step.readUri))) {
118
+ return `${step.uri} is read only`;
123
119
  }
124
120
  continue;
125
121
  }
126
- if (step.kind === 'rename') {
127
- const oldPath = fileUriToPath(step.oldUri);
128
- const newPath = fileUriToPath(step.newUri);
129
- if (oldPath === null || newPath === null || host.files === null) {
130
- return `${step.oldUri} cannot be moved from here`;
131
- }
132
- origins.set(step.newUri, origins.get(step.oldUri) ?? step.oldUri);
133
- planned.push({ kind: 'rename', oldPath, newPath });
134
- continue;
122
+ const uris = step.kind === 'rename' ? [step.oldUri, step.newUri] : [step.uri];
123
+ const paths = uris.map((uri) => fileUriToPath(uri));
124
+ if (paths.includes(null)) {
125
+ return `${uris[0]} cannot be changed from here`;
135
126
  }
136
- const source = origins.get(step.uri) ?? step.uri;
137
- const created = creates.get(source);
138
- if (created !== undefined) {
139
- try {
140
- created.text = applyTextEdits(created.text, step.edits);
141
- }
142
- catch (error) {
143
- return messageOf(error);
127
+ if ((step.kind === 'create' || step.kind === 'rename') && step.overwrite) {
128
+ const target = step.kind === 'create' ? step.uri : step.newUri;
129
+ const refusal = overwriteProblem(step, target, placesOf(target, renames), host, reads);
130
+ if (refusal !== null) {
131
+ return refusal;
144
132
  }
145
- continue;
146
133
  }
147
- if (!texts.has(source)) {
148
- const editor = host.editorOf(source);
149
- if (editor) {
150
- texts.set(source, { text: editor.getText(), disk: null });
151
- }
152
- else {
153
- const path = fileUriToPath(source);
154
- const disk = path === null || host.files === null ? null : await host.files.read(path);
134
+ if (step.kind === 'rename') {
135
+ renames.push({ kind: 'rename', oldUri: step.oldUri, newUri: step.newUri });
136
+ }
137
+ if (step.kind === 'delete' && losesWork(step.recursive, placesOf(step.uri, renames), host)) {
138
+ return `${paths[0]} has changes that are not saved`;
139
+ }
140
+ }
141
+ for (const [uri, planned] of reads.open) {
142
+ const editor = host.editorOf(uri);
143
+ if (editor === undefined || editor.getText() !== planned.text) {
144
+ return `${uri} changed while the edit was being prepared`;
145
+ }
146
+ const version = host.versionOf?.(uri) ?? null;
147
+ if (planned.version !== null && version !== planned.version) {
148
+ return `${uri} changed while the edit was being prepared`;
149
+ }
150
+ }
151
+ for (const uri of reads.disks.keys()) {
152
+ if (host.editorOf(uri) !== undefined) {
153
+ return `${uri} was opened while the edit was being prepared`;
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ /*
159
+ * Applies an LSP workspace edit step by step, after planning it against every text (`planWorkspaceOperations`)
160
+ * so one that does not fit changes nothing. What the host supports follows `host.files`: moves need `rename`,
161
+ * makes need `create` and deletes need `delete`. Just before the first step runs, every open editor must still
162
+ * hold the text and version the plan was made from, and a delete must not lose unsaved work. Each text step
163
+ * and each delete checks that again right before it runs, since the steps before it are awaited.
164
+ *
165
+ * A file an editor holds takes its edits as one undo step per step of the edit; any other file gets an unsaved
166
+ * draft, since nothing is written to disk on a server's word alone, unless the edit also moves files: then it
167
+ * is one change, saved and moved through the machine. A create or a move over a file that is there is refused,
168
+ * unless it says `overwrite` and `files.supportsOverwrite` is set. Then it is refused only when the target, or
169
+ * anything under a folder target, has unsaved work, checked again right before the step runs. `overwrite` wins
170
+ * over `ignoreIfExists`, as the protocol says. A created file that an editor still holds shows its new text.
171
+ * The steps run in order and a step that no longer fits stops the edit there, with the steps before it applied
172
+ * and nothing rolled back. A folder that only exists because an earlier move in the same edit made it is
173
+ * looked for where it was and refused.
174
+ */
175
+ export async function applyWorkspaceEdit(edit, host) {
176
+ const reads = { open: new Map(), disks: new Map() };
177
+ const files = host.files;
178
+ let steps;
179
+ try {
180
+ ({ steps } = await planWorkspaceOperations(edit, {
181
+ operations: supportedResourceOperations(files),
182
+ read: async (uri) => {
183
+ const editor = host.editorOf(uri);
184
+ if (editor !== undefined) {
185
+ const text = editor.getText();
186
+ const version = host.versionOf?.(uri) ?? null;
187
+ reads.open.set(uri, { text, version });
188
+ return { text, version, open: true };
189
+ }
190
+ const path = fileUriToPath(uri);
191
+ const disk = path === null || files === null ? null : await files.read(path);
155
192
  if (disk === null) {
156
- return `${source} cannot be read as text`;
193
+ return null;
157
194
  }
158
- texts.set(source, { text: disk.text, disk });
159
- }
195
+ reads.disks.set(uri, disk);
196
+ return { text: disk.text, version: null, open: false };
197
+ },
198
+ ...(files?.exists === undefined
199
+ ? {}
200
+ : {
201
+ exists: async (uri) => {
202
+ const path = fileUriToPath(uri);
203
+ return path !== null && (await files.exists(path));
204
+ }
205
+ }),
206
+ ...(files?.isFolder === undefined
207
+ ? {}
208
+ : {
209
+ isFolder: async (uri) => {
210
+ const path = fileUriToPath(uri);
211
+ return path === null || (await files.isFolder(path));
212
+ }
213
+ })
214
+ }));
215
+ }
216
+ catch (error) {
217
+ return refused(messageOf(error));
218
+ }
219
+ const problem = problemWith(steps, host, reads);
220
+ if (problem !== null) {
221
+ return refused(problem);
222
+ }
223
+ const running = { ahead: [], reads, saving: steps.some((step) => step.kind === 'rename'), staged: [], touched: new Set() };
224
+ for (const [index, step] of steps.entries()) {
225
+ running.ahead = steps.slice(index + 1);
226
+ const reason = await run(step, host, running);
227
+ if (reason !== null) {
228
+ return refused(reason);
160
229
  }
161
- const held = texts.get(source);
162
- let after;
163
- try {
164
- after = applyTextEdits(held.text, step.edits);
230
+ }
231
+ if (running.staged.length > 0) {
232
+ files?.stage(running.staged);
233
+ }
234
+ return { applied: true };
235
+ }
236
+ /* Why a text step no longer fits its editor, now that the steps before it ran; null when it does. */
237
+ function driftOf(step, host, uri, editor, touched) {
238
+ if (editor.getText() !== step.before) {
239
+ return `${step.uri} changed while the edit was being applied`;
240
+ }
241
+ const version = uri === step.readUri && !touched.has(editor) ? host.versionOf?.(uri) : undefined;
242
+ if (version !== undefined && step.version !== null && version !== step.version) {
243
+ return `${step.uri} changed while the edit was being applied`;
244
+ }
245
+ return readOnlyAt(host, uri, editor) ? `${step.uri} is read only` : null;
246
+ }
247
+ /* Does one step; the reason it could not, or null. */
248
+ async function run(step, host, running) {
249
+ const files = host.files;
250
+ if (step.kind === 'create') {
251
+ return step.overwrite ? overwriteFile(step, host) : files.create(fileUriToPath(step.uri), step.text);
252
+ }
253
+ if (step.kind === 'rename') {
254
+ const [from, to] = [fileUriToPath(step.oldUri), fileUriToPath(step.newUri)];
255
+ if (!step.overwrite) {
256
+ return files.rename(from, to);
165
257
  }
166
- catch (error) {
167
- return messageOf(error);
258
+ // The steps before this one ran, so the unsaved work at the target is looked for once more.
259
+ return losesWork(!running.reads.disks.has(step.newUri) && !running.reads.open.has(step.newUri), [step.newUri], host)
260
+ ? `${to} has changes that are not saved`
261
+ : files.rename(from, to, { overwrite: true });
262
+ }
263
+ if (step.kind === 'delete') {
264
+ const path = fileUriToPath(step.uri);
265
+ if (losesWork(step.recursive, [step.uri], host)) {
266
+ return `${path} has changes that are not saved`;
168
267
  }
169
- planned.push({ kind: 'edits', uri: step.uri, edits: step.edits, before: held.text, after, disk: held.disk });
170
- texts.set(source, { text: after, disk: held.disk });
268
+ return files.delete(path, { recursive: step.recursive });
171
269
  }
172
- return planned;
270
+ if (step.after === step.before) {
271
+ return null;
272
+ }
273
+ if (step.open) {
274
+ const uri = host.editorOf(step.uri) === undefined ? step.readUri : step.uri;
275
+ return editOpen(step, host, running, uri, host.editorOf(uri));
276
+ }
277
+ return runDraft(step, host, running);
278
+ }
279
+ /* Empties a file that is there and gives it its text; the editor that still holds it is set to the same text. */
280
+ async function overwriteFile(step, host) {
281
+ const path = fileUriToPath(step.uri);
282
+ if (losesWork(false, [step.uri], host)) {
283
+ return `${path} has changes that are not saved`;
284
+ }
285
+ const reason = await host.files.create(path, step.text, { overwrite: true });
286
+ if (reason === null) {
287
+ host.editorOf(step.uri)?.setText(step.text);
288
+ }
289
+ return reason;
290
+ }
291
+ function editOpen(step, host, running, uri, editor) {
292
+ if (editor === undefined) {
293
+ return `${step.uri} is not open any more`;
294
+ }
295
+ const drift = driftOf(step, host, uri, editor, running.touched);
296
+ if (drift !== null) {
297
+ return drift;
298
+ }
299
+ running.touched.add(editor);
300
+ // The edits stay apart, so the caret and what is drawn over the text stay where they were between them.
301
+ return editor.applyEdits(step.edits.map(contentChangeOf)) ? null : `${step.uri} is read only`;
302
+ }
303
+ /* Whether a later step deletes the file or a folder around it, so a draft or a save of it would only bring it back. */
304
+ function deletedAhead(uri, ahead) {
305
+ return ahead.some((step) => step.kind === 'delete' && (step.uri === uri || within(uri, step.uri.replace(/\/$/, ''))));
173
306
  }
174
- /* A file that is not there yet, or null when it is and the create says to leave it. Making one over a file that is there is not supported. */
175
- async function planCreate(file, host, texts) {
176
- const path = fileUriToPath(file.uri);
177
- if (path === null || host.files?.create === undefined) {
178
- return `Creating ${file.uri} is not supported here`;
307
+ async function runDraft(step, host, running) {
308
+ const { reads, saving, staged, ahead } = running;
309
+ const uri = [step.uri, step.readUri].find((candidate) => host.editorOf(candidate) !== undefined);
310
+ if (uri !== undefined) {
311
+ return editOpen(step, host, running, uri, host.editorOf(uri));
179
312
  }
180
- const there = texts.has(file.uri) || host.editorOf(file.uri) !== undefined || (await host.files.read(path)) !== null;
181
- if (!there) {
182
- return { kind: 'create', path, text: '' };
313
+ const path = fileUriToPath(step.uri);
314
+ const disk = reads.disks.get(step.readUri);
315
+ if (path === null || disk === undefined || deletedAhead(step.uri, ahead)) {
316
+ return null;
317
+ }
318
+ const file = { path, disk, text: step.after };
319
+ if (!saving) {
320
+ staged.push(file);
321
+ return null;
183
322
  }
184
- return file.options?.ignoreIfExists === true && file.options.overwrite !== true ? null : `${path} already exists`;
323
+ return host.files.save([file]);
185
324
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-react",
3
- "version": "0.22.1",
3
+ "version": "0.23.1",
4
4
  "type": "module",
5
5
  "description": "Language-enabled editors, feature coordination and React popups over injected language services.",
6
6
  "license": "FSL-1.1-MIT",
@@ -49,6 +49,11 @@
49
49
  "./locales/*.json": {
50
50
  "source": "./src/locales/*.json",
51
51
  "default": "./dist/locales/*.json"
52
+ },
53
+ "./snippet-transform-worker": {
54
+ "source": "./src/snippet-transform-worker.ts",
55
+ "types": "./dist/snippet-transform-worker.d.ts",
56
+ "default": "./dist/snippet-transform-worker.js"
52
57
  }
53
58
  },
54
59
  "scripts": {
@@ -57,9 +62,9 @@
57
62
  "test": "bun test --conditions=source"
58
63
  },
59
64
  "dependencies": {
60
- "@adecore/editor": "0.22.1",
61
- "@adecore/lsp": "0.22.1",
62
- "@adecore/merge": "0.22.1",
65
+ "@adecore/editor": "0.23.1",
66
+ "@adecore/lsp": "0.23.1",
67
+ "@adecore/merge": "0.23.1",
63
68
  "lucide-react": "^1.47.0",
64
69
  "react-markdown": "^10.1.0",
65
70
  "remark-breaks": "^4.0.0",
@@ -67,7 +72,7 @@
67
72
  "zustand": "^5.0.15"
68
73
  },
69
74
  "peerDependencies": {
70
- "@adecore/ui": "^0.22.1",
75
+ "@adecore/ui": "^0.23.1",
71
76
  "i18next": "^26.4.2",
72
77
  "react": "^19.3.0",
73
78
  "react-dom": "^19.3.0",