@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,25 @@
1
+ import { useMemo, useSyncExternalStore } from 'react';
2
+ import type { EditorLanguage } from './editor-language.ts';
3
+ import type { ProblemCounts } from './diagnostics.ts';
4
+
5
+ const NONE: ProblemCounts = { error: 0, warning: 0, info: 0 };
6
+
7
+ function countsStore(language: EditorLanguage | null): { get(): ProblemCounts; subscribe(listener: () => void): () => void } {
8
+ let current = language?.diagnostics.counts() ?? NONE;
9
+ return {
10
+ get: () => {
11
+ const next = language?.diagnostics.counts() ?? NONE;
12
+ if (next.error !== current.error || next.warning !== current.warning || next.info !== current.info) {
13
+ current = next;
14
+ }
15
+ return current;
16
+ },
17
+ subscribe: (listener) => language?.diagnostics.onChange(listener) ?? (() => undefined)
18
+ };
19
+ }
20
+
21
+ /* How many errors, warnings and infos the file has, said again when the servers report. */
22
+ export function useProblemCounts(language: EditorLanguage | null): ProblemCounts {
23
+ const store = useMemo(() => countsStore(language), [language]);
24
+ return useSyncExternalStore(store.subscribe, store.get);
25
+ }
@@ -0,0 +1,88 @@
1
+ import type { EditorFolds, FoldRole } from '@adecore/editor';
2
+ export interface RevealLineRequest {
3
+ readonly line: number;
4
+ }
5
+ function endpointKey(namespace: string, path: string): string {
6
+ return `${namespace}:${path}`;
7
+ }
8
+
9
+ export interface ViewState {
10
+ readonly scrollTop: number;
11
+ /* One-based, as the editor opens on it. */
12
+ readonly line: number;
13
+ readonly column: number;
14
+ /* What was folded, which the editor folds again. */
15
+ readonly folds: EditorFolds;
16
+ }
17
+
18
+ /* Where each file's editor was when it went, so switching tabs or a node scrolling back into view opens it there again. Per window, never stored. */
19
+ export const viewStates = new Map<string, ViewState>();
20
+
21
+ /*
22
+ * A line asked for wins; otherwise where this file's editor last was, and for a file opened the first time, where the
23
+ * viewer before it was scrolled and with the kinds of fold the settings choose folded. What was folded comes back
24
+ * whichever way it opens.
25
+ */
26
+ export function openingPlace(
27
+ reveal: RevealLineRequest | null,
28
+ last: ViewState | undefined,
29
+ placeholderScroll: number,
30
+ foldDefaults: readonly FoldRole[]
31
+ ): { line?: number; column?: number; scrollTop?: number; folds?: EditorFolds; foldDefaults?: readonly FoldRole[] } {
32
+ const folds = last === undefined ? { foldDefaults } : { folds: last.folds };
33
+ if (reveal !== null) {
34
+ return { line: reveal.line, ...folds };
35
+ }
36
+ if (last !== undefined) {
37
+ return { line: last.line, column: last.column, scrollTop: last.scrollTop, ...folds };
38
+ }
39
+ return { scrollTop: placeholderScroll, ...folds };
40
+ }
41
+
42
+ /* Where a place the editors know by the key it had goes now: a file, or the files of a folder that moved. */
43
+ const moves: Array<{ from: string; to: string }> = [];
44
+
45
+ function movedKey(key: string, from: string, to: string): string | null {
46
+ if (key === from) {
47
+ return to;
48
+ }
49
+ return key.startsWith(`${from}/`) ? `${to}${key.slice(from.length)}` : null;
50
+ }
51
+
52
+ /*
53
+ * A file or folder moved, so where its editors were follows it. An editor that is still open writes its place once
54
+ * more under the key it mounted with as it closes, which `viewStateKey` sends to the new one; a new editor on the
55
+ * old path forgets the move.
56
+ */
57
+ export function followViewStates(endpointId: string, from: string, to: string): void {
58
+ const oldKey = endpointKey(endpointId, from);
59
+ const newKey = endpointKey(endpointId, to);
60
+ for (const [key, state] of [...viewStates]) {
61
+ const target = movedKey(key, oldKey, newKey);
62
+ if (target !== null) {
63
+ viewStates.delete(key);
64
+ viewStates.set(target, state);
65
+ }
66
+ }
67
+ moves.push({ from: oldKey, to: newKey });
68
+ }
69
+
70
+ /* The key an editor that mounted under `key` leaves its place under. */
71
+ export function viewStateKey(key: string): string {
72
+ for (const { from, to } of moves) {
73
+ const target = movedKey(key, from, to);
74
+ if (target !== null) {
75
+ return target;
76
+ }
77
+ }
78
+ return key;
79
+ }
80
+
81
+ /* An editor mounts under `key`, which is a file of its own again whatever moved away from there. */
82
+ export function forgetMovesFrom(key: string): void {
83
+ for (let i = moves.length - 1; i >= 0; i--) {
84
+ if (movedKey(key, moves[i]!.from, moves[i]!.to) !== null) {
85
+ moves.splice(i, 1);
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,35 @@
1
+ import { diffLines } from '@adecore/merge';
2
+
3
+ /* A word, a run of spaces or one other character, so a change inside a line is found by word and not by letter. */
4
+ const TOKEN = /\w+|\s+|[^\w\s]/g;
5
+
6
+ /*
7
+ * The character ranges of each removed line that the added lines changed, `[start, end)` per line, for
8
+ * the stronger tint on the words a change replaced. Lines are only compared when as many were added as
9
+ * removed, since then each removed line has the line it became; any other change has no such pairing
10
+ * and draws no words.
11
+ */
12
+ export function replacedWords(removed: readonly string[], added: readonly string[]): Array<Array<[number, number]>> {
13
+ return removed.map((line, index) => (removed.length === added.length ? replacedRanges(line, added[index]!) : []));
14
+ }
15
+
16
+ function replacedRanges(removed: string, added: string): Array<[number, number]> {
17
+ const before = removed.match(TOKEN) ?? [];
18
+ const after = added.match(TOKEN) ?? [];
19
+ const starts: number[] = [];
20
+ let offset = 0;
21
+ for (const token of before) {
22
+ starts.push(offset);
23
+ offset += token.length;
24
+ }
25
+ starts.push(offset);
26
+ const ranges: Array<[number, number]> = [];
27
+ for (const change of diffLines(before, after)) {
28
+ const text = removed.slice(starts[change.baseStart]!, starts[change.baseEnd]!);
29
+ if (text.trim() !== '') {
30
+ const start = starts[change.baseStart]! + (text.length - text.trimStart().length);
31
+ ranges.push([start, start + text.trim().length]);
32
+ }
33
+ }
34
+ return ranges;
35
+ }
@@ -0,0 +1,194 @@
1
+ import type { Editor, EditorContentChange } from '@adecore/editor';
2
+ import { applyTextEdits, fileUriToPath, type ApplyWorkspaceEditResult, type RenameFile, type TextEdit, type WorkspaceEdit } from '@adecore/lsp';
3
+ import type { DiskText } from './host-types.ts';
4
+
5
+ /* A file that changed without an editor of this project holding it: its new text waits as an unsaved draft. */
6
+ export interface StagedFile {
7
+ readonly path: string;
8
+ readonly disk: DiskText;
9
+ readonly text: string;
10
+ }
11
+
12
+ export interface ProjectFiles {
13
+ /* The text of a file that is no open document, as a draft holds it or the machine has it; null when it is not a text file. */
14
+ read(path: string): Promise<DiskText | null>;
15
+ /* Puts the new texts in the files' drafts and tells the person. Nothing is written to disk. */
16
+ stage(files: readonly StagedFile[]): void;
17
+ /* Writes the new texts through the machine; the reason of the first file that could not be saved, or null. */
18
+ save(files: readonly StagedFile[]): Promise<string | null>;
19
+ /* Moves a file or folder on the machine, with what is open on it, and tells the servers it moved; the reason it could not, or null. */
20
+ rename(from: string, to: string): Promise<string | null>;
21
+ }
22
+
23
+ export interface WorkspaceEditHost {
24
+ /* The editor whose document this is, which takes the edit as one undo step. */
25
+ editorOf(uri: string): Editor | undefined;
26
+ readonly files: ProjectFiles | null;
27
+ }
28
+
29
+ function refused(failureReason: string): ApplyWorkspaceEditResult {
30
+ return { applied: false, failureReason };
31
+ }
32
+
33
+ /* One thing an edit does, in the order it says: text edits of a document, or a file that moves. */
34
+ type Step =
35
+ | { readonly kind: 'edits'; readonly uri: string; readonly edits: TextEdit[] }
36
+ | { readonly kind: 'rename'; readonly oldUri: string; readonly newUri: string };
37
+
38
+ /* The steps of an edit; null when it creates or deletes a file, which an edit is not applied with. */
39
+ function stepsOf(edit: WorkspaceEdit): Step[] | null {
40
+ const steps: Step[] = [];
41
+ for (const change of edit.documentChanges ?? []) {
42
+ if ('textDocument' in change) {
43
+ steps.push({ kind: 'edits', uri: change.textDocument.uri, edits: change.edits });
44
+ } else if (change.kind === 'rename') {
45
+ steps.push({ kind: 'rename', oldUri: change.oldUri, newUri: change.newUri });
46
+ } else {
47
+ return null;
48
+ }
49
+ }
50
+ if (edit.documentChanges === undefined) {
51
+ for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
52
+ steps.push({ kind: 'edits', uri, edits });
53
+ }
54
+ }
55
+ return steps;
56
+ }
57
+
58
+ /* The text edits of an edit per document, as the entries a document has in it; null when it creates or deletes a file. */
59
+ export function entriesOf(edit: WorkspaceEdit): Map<string, TextEdit[][]> | null {
60
+ const steps = stepsOf(edit);
61
+ if (steps === null) {
62
+ return null;
63
+ }
64
+ const entries = new Map<string, TextEdit[][]>();
65
+ for (const step of steps) {
66
+ if (step.kind === 'edits') {
67
+ entries.set(step.uri, [...(entries.get(step.uri) ?? []), step.edits]);
68
+ }
69
+ }
70
+ return entries;
71
+ }
72
+
73
+ /* The files an edit moves, which a preview of lines cannot show. */
74
+ export function renamesOf(edit: WorkspaceEdit): RenameFile[] {
75
+ return (edit.documentChanges ?? []).filter((change): change is RenameFile => 'kind' in change && change.kind === 'rename');
76
+ }
77
+
78
+ function contentChangeOf(edit: TextEdit): EditorContentChange {
79
+ return { range: edit.range, text: edit.newText };
80
+ }
81
+
82
+ /* What a step of text edits does to one document, worked out before anything changes. */
83
+ interface PlannedEdits {
84
+ readonly kind: 'edits';
85
+ readonly uri: string;
86
+ readonly edits: TextEdit[];
87
+ readonly before: string;
88
+ readonly after: string;
89
+ /* Set when no editor holds the document: what the machine has, which the new text is a draft of. */
90
+ readonly disk: DiskText | null;
91
+ }
92
+
93
+ type Planned = PlannedEdits | { readonly kind: 'rename'; readonly oldPath: string; readonly newPath: string };
94
+
95
+ /*
96
+ * Applies an LSP workspace edit on the client, step by step in the order it says. A file an editor holds takes
97
+ * its edits as one undo step. Any other file gets its new text as an unsaved draft that a person saves, and
98
+ * nothing is written to disk on a server's word alone, unless the edit also moves a file: the edits and the
99
+ * move are then one change, so every file is saved through the machine and the move is made there. The
100
+ * edit is checked against every text first, so one that does not fit changes nothing.
101
+ */
102
+ export async function applyWorkspaceEdit(edit: WorkspaceEdit, host: WorkspaceEditHost): Promise<ApplyWorkspaceEditResult> {
103
+ const steps = stepsOf(edit);
104
+ if (steps === null) {
105
+ return refused('Creating and deleting files is not supported yet');
106
+ }
107
+ const planned = await plan(steps, host);
108
+ if (typeof planned === 'string') {
109
+ return refused(planned);
110
+ }
111
+ const moving = planned.some((item) => item.kind === 'rename');
112
+ const staged: StagedFile[] = [];
113
+ for (const item of planned) {
114
+ if (item.kind === 'rename') {
115
+ const reason = await host.files!.rename(item.oldPath, item.newPath);
116
+ if (reason !== null) {
117
+ return refused(reason);
118
+ }
119
+ continue;
120
+ }
121
+ if (item.after === item.before) {
122
+ continue;
123
+ }
124
+ const editor = host.editorOf(item.uri);
125
+ if (editor) {
126
+ // The edits stay apart, so the caret and what is drawn over the text stay where they were between them.
127
+ if (!editor.applyEdits(item.edits.map(contentChangeOf))) {
128
+ return refused(`${item.uri} is read only`);
129
+ }
130
+ continue;
131
+ }
132
+ const path = fileUriToPath(item.uri);
133
+ if (path === null || item.disk === null) {
134
+ continue;
135
+ }
136
+ const file = { path, disk: item.disk, text: item.after };
137
+ if (!moving) {
138
+ staged.push(file);
139
+ continue;
140
+ }
141
+ const reason = await host.files!.save([file]);
142
+ if (reason !== null) {
143
+ return refused(reason);
144
+ }
145
+ }
146
+ if (staged.length > 0) {
147
+ host.files?.stage(staged);
148
+ }
149
+ return { applied: true };
150
+ }
151
+
152
+ /* Reads every text the edit needs and works out what each step leaves, or says why it does not fit. */
153
+ async function plan(steps: readonly Step[], host: WorkspaceEditHost): Promise<Planned[] | string> {
154
+ const texts = new Map<string, { text: string; disk: DiskText | null }>();
155
+ // A file that has moved is read where it still stands: nothing moves until the plan is made.
156
+ const origins = new Map<string, string>();
157
+ const planned: Planned[] = [];
158
+ for (const step of steps) {
159
+ if (step.kind === 'rename') {
160
+ const oldPath = fileUriToPath(step.oldUri);
161
+ const newPath = fileUriToPath(step.newUri);
162
+ if (oldPath === null || newPath === null || host.files === null) {
163
+ return `${step.oldUri} cannot be moved from here`;
164
+ }
165
+ origins.set(step.newUri, origins.get(step.oldUri) ?? step.oldUri);
166
+ planned.push({ kind: 'rename', oldPath, newPath });
167
+ continue;
168
+ }
169
+ const source = origins.get(step.uri) ?? step.uri;
170
+ if (!texts.has(source)) {
171
+ const editor = host.editorOf(source);
172
+ if (editor) {
173
+ texts.set(source, { text: editor.getText(), disk: null });
174
+ } else {
175
+ const path = fileUriToPath(source);
176
+ const disk = path === null || host.files === null ? null : await host.files.read(path);
177
+ if (disk === null) {
178
+ return `${source} cannot be read as text`;
179
+ }
180
+ texts.set(source, { text: disk.text, disk });
181
+ }
182
+ }
183
+ const held = texts.get(source)!;
184
+ let after: string;
185
+ try {
186
+ after = applyTextEdits(held.text, step.edits);
187
+ } catch (error) {
188
+ return error instanceof Error ? error.message : String(error);
189
+ }
190
+ planned.push({ kind: 'edits', uri: step.uri, edits: step.edits, before: held.text, after, disk: held.disk });
191
+ texts.set(source, { text: after, disk: held.disk });
192
+ }
193
+ return planned;
194
+ }