@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.
- package/dist/CallHierarchyPanel.d.ts +6 -0
- package/dist/CallHierarchyPanel.js +92 -0
- package/dist/CompletionPopup.js +11 -3
- package/dist/DiffView.d.ts +22 -0
- package/dist/DiffView.js +203 -0
- package/dist/EditorContextMenu.js +1 -0
- package/dist/EditorView.js +18 -1
- package/dist/FindReplace.d.ts +1 -1
- package/dist/FindReplace.js +41 -11
- package/dist/HoverCard.js +2 -2
- package/dist/LanguagePopups.js +4 -1
- package/dist/Markdown.d.ts +2 -1
- package/dist/Markdown.js +8 -4
- package/dist/PanelResizeHandle.d.ts +8 -0
- package/dist/PanelResizeHandle.js +71 -0
- package/dist/PeekPanel.d.ts +3 -0
- package/dist/PeekPanel.js +7 -4
- package/dist/PickPopup.js +15 -2
- package/dist/RenameCard.js +10 -5
- package/dist/RenderedDocs.d.ts +9 -0
- package/dist/RenderedDocs.js +84 -0
- package/dist/SplitDiffView.d.ts +9 -0
- package/dist/SplitDiffView.js +276 -0
- package/dist/SymbolPicker.d.ts +3 -0
- package/dist/SymbolPicker.js +4 -3
- package/dist/call-hierarchy-model.d.ts +52 -0
- package/dist/call-hierarchy-model.js +93 -0
- package/dist/call-hierarchy.d.ts +42 -0
- package/dist/call-hierarchy.js +398 -0
- package/dist/code-actions-model.d.ts +2 -0
- package/dist/code-actions-model.js +12 -10
- package/dist/code-actions.js +3 -1
- package/dist/completion-model.d.ts +14 -4
- package/dist/completion-model.js +40 -33
- package/dist/completion.d.ts +4 -0
- package/dist/completion.js +257 -42
- package/dist/diagnostics.js +8 -6
- package/dist/diff-expansion.d.ts +9 -0
- package/dist/diff-expansion.js +41 -0
- package/dist/diff-overlays.d.ts +15 -0
- package/dist/diff-overlays.js +28 -0
- package/dist/diff-syntax.d.ts +16 -0
- package/dist/diff-syntax.js +49 -0
- package/dist/diff-wordmarks.d.ts +5 -0
- package/dist/diff-wordmarks.js +26 -0
- package/dist/doc-comment-model.d.ts +23 -0
- package/dist/doc-comment-model.js +291 -0
- package/dist/editor-language.d.ts +13 -1
- package/dist/editor-language.js +15 -5
- package/dist/editor-react.css +79 -0
- package/dist/format-on-type.d.ts +14 -0
- package/dist/format-on-type.js +96 -0
- package/dist/host-types.d.ts +19 -0
- package/dist/hover.d.ts +4 -0
- package/dist/hover.js +15 -2
- package/dist/index.d.ts +19 -5
- package/dist/index.js +9 -1
- package/dist/inlay-hints.d.ts +47 -2
- package/dist/inlay-hints.js +353 -16
- package/dist/language-ids.js +4 -2
- package/dist/locales/en.json +117 -3
- package/dist/locales/nl.json +117 -3
- package/dist/models.d.ts +6 -2
- package/dist/models.js +5 -1
- package/dist/panel-size.d.ts +6 -0
- package/dist/panel-size.js +25 -0
- package/dist/patch-decorations.d.ts +27 -0
- package/dist/patch-decorations.js +182 -0
- package/dist/patch-layout.d.ts +40 -0
- package/dist/patch-layout.js +304 -0
- package/dist/peek.d.ts +3 -1
- package/dist/peek.js +6 -1
- package/dist/pick.d.ts +1 -0
- package/dist/pick.js +11 -1
- package/dist/popups.d.ts +17 -0
- package/dist/popups.js +1 -0
- package/dist/project-language.d.ts +12 -0
- package/dist/project-language.js +52 -2
- package/dist/refresher.d.ts +1 -0
- package/dist/refresher.js +5 -0
- package/dist/rename-model.d.ts +1 -1
- package/dist/rename-model.js +3 -7
- package/dist/rename.js +5 -8
- package/dist/rendered-docs.d.ts +64 -0
- package/dist/rendered-docs.js +374 -0
- package/dist/row-host.d.ts +2 -0
- package/dist/row-host.js +11 -3
- package/dist/semantic-delta.d.ts +2 -0
- package/dist/semantic-delta.js +27 -0
- package/dist/semantic-model.js +16 -0
- package/dist/semantic-tokens.d.ts +26 -1
- package/dist/semantic-tokens.js +174 -14
- package/dist/shortcut-keys.d.ts +2 -0
- package/dist/shortcut-keys.js +6 -2
- package/dist/signature.js +1 -1
- package/dist/snippet-field-transforms.d.ts +30 -0
- package/dist/snippet-field-transforms.js +116 -0
- package/dist/snippet-indent.d.ts +2 -0
- package/dist/snippet-indent.js +49 -0
- package/dist/snippet-range-order.d.ts +3 -0
- package/dist/snippet-range-order.js +19 -0
- package/dist/snippet-session.d.ts +42 -6
- package/dist/snippet-session.js +650 -53
- package/dist/snippet-transform-client.d.ts +19 -0
- package/dist/snippet-transform-client.js +66 -0
- package/dist/snippet-transform-format.d.ts +15 -0
- package/dist/snippet-transform-format.js +302 -0
- package/dist/snippet-transform-protocol.d.ts +45 -0
- package/dist/snippet-transform-protocol.js +8 -0
- package/dist/snippet-transform-queue.d.ts +32 -0
- package/dist/snippet-transform-queue.js +173 -0
- package/dist/snippet-transform-worker.d.ts +10 -0
- package/dist/snippet-transform-worker.js +41 -0
- package/dist/snippet-variable-names.d.ts +1 -0
- package/dist/snippet-variable-names.js +36 -0
- package/dist/snippet-variables.d.ts +7 -0
- package/dist/snippet-variables.js +66 -0
- package/dist/snippet.d.ts +30 -3
- package/dist/snippet.js +402 -87
- package/dist/split-alignment.d.ts +6 -0
- package/dist/split-alignment.js +38 -0
- package/dist/split-scroll.d.ts +14 -0
- package/dist/split-scroll.js +45 -0
- package/dist/symbols.d.ts +2 -1
- package/dist/symbols.js +39 -7
- package/dist/testing.d.ts +5 -1
- package/dist/testing.js +12 -0
- package/dist/timers.d.ts +1 -0
- package/dist/timers.js +4 -0
- package/dist/use-panel-body.d.ts +6 -0
- package/dist/use-panel-body.js +13 -0
- package/dist/workspace-edit.d.ts +25 -3
- package/dist/workspace-edit.js +282 -143
- package/package.json +10 -5
- package/src/CallHierarchyPanel.tsx +244 -0
- package/src/CompletionPopup.tsx +12 -1
- package/src/DiffView.tsx +262 -0
- package/src/EditorContextMenu.tsx +1 -0
- package/src/EditorView.tsx +21 -2
- package/src/FindReplace.tsx +51 -13
- package/src/HoverCard.tsx +23 -21
- package/src/LanguagePopups.tsx +5 -0
- package/src/Markdown.tsx +10 -3
- package/src/PanelResizeHandle.tsx +104 -0
- package/src/PeekPanel.tsx +11 -6
- package/src/PickPopup.tsx +18 -2
- package/src/RenameCard.tsx +18 -0
- package/src/RenderedDocs.tsx +155 -0
- package/src/SplitDiffView.tsx +337 -0
- package/src/SymbolPicker.tsx +4 -3
- package/src/call-hierarchy-model.ts +151 -0
- package/src/call-hierarchy.ts +440 -0
- package/src/code-actions-model.ts +17 -11
- package/src/code-actions.ts +3 -1
- package/src/completion-model.ts +61 -35
- package/src/completion.ts +302 -42
- package/src/diagnostics.ts +8 -6
- package/src/diff-expansion.ts +49 -0
- package/src/diff-overlays.tsx +69 -0
- package/src/diff-syntax.ts +67 -0
- package/src/diff-wordmarks.ts +31 -0
- package/src/doc-comment-model.ts +336 -0
- package/src/editor-language.ts +22 -6
- package/src/editor-react.css +79 -0
- package/src/format-on-type.ts +111 -0
- package/src/host-types.ts +23 -0
- package/src/hover.ts +18 -3
- package/src/index.ts +27 -4
- package/src/inlay-hints.ts +394 -18
- package/src/language-ids.ts +4 -2
- package/src/locales/en.json +117 -3
- package/src/locales/nl.json +117 -3
- package/src/models.ts +40 -1
- package/src/panel-size.ts +28 -0
- package/src/patch-decorations.ts +224 -0
- package/src/patch-layout.ts +388 -0
- package/src/peek.ts +6 -1
- package/src/pick.ts +12 -1
- package/src/popups.ts +24 -0
- package/src/project-language.ts +63 -2
- package/src/refresher.ts +6 -0
- package/src/rename-model.ts +4 -8
- package/src/rename.ts +5 -8
- package/src/rendered-docs.ts +430 -0
- package/src/row-host.ts +12 -3
- package/src/semantic-delta.ts +29 -0
- package/src/semantic-model.ts +17 -0
- package/src/semantic-tokens.ts +202 -19
- package/src/shortcut-keys.ts +6 -2
- package/src/signature.ts +1 -1
- package/src/snippet-field-transforms.ts +144 -0
- package/src/snippet-indent.ts +53 -0
- package/src/snippet-range-order.ts +19 -0
- package/src/snippet-session.ts +797 -56
- package/src/snippet-transform-client.ts +84 -0
- package/src/snippet-transform-format.ts +325 -0
- package/src/snippet-transform-protocol.ts +24 -0
- package/src/snippet-transform-queue.ts +199 -0
- package/src/snippet-transform-worker.ts +58 -0
- package/src/snippet-variable-names.ts +36 -0
- package/src/snippet-variables.ts +74 -0
- package/src/snippet.ts +452 -89
- package/src/split-alignment.ts +45 -0
- package/src/split-scroll.ts +54 -0
- package/src/symbols.ts +43 -7
- package/src/testing.ts +25 -0
- package/src/timers.ts +5 -0
- package/src/use-panel-body.ts +17 -0
- package/src/workspace-edit.ts +347 -169
package/src/workspace-edit.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { Editor, EditorContentChange } from '@adecore/editor';
|
|
2
2
|
import {
|
|
3
|
-
applyTextEdits,
|
|
4
3
|
fileUriToPath,
|
|
4
|
+
planWorkspaceOperations,
|
|
5
5
|
type ApplyWorkspaceEditResult,
|
|
6
|
-
type
|
|
6
|
+
type PlannedCreateStep,
|
|
7
|
+
type PlannedRenameStep,
|
|
8
|
+
type PlannedTextStep,
|
|
9
|
+
type ResourceOperationKind,
|
|
7
10
|
type RenameFile,
|
|
8
11
|
type TextEdit,
|
|
9
|
-
type WorkspaceEdit
|
|
12
|
+
type WorkspaceEdit,
|
|
13
|
+
type WorkspaceEditStep
|
|
10
14
|
} from '@adecore/lsp';
|
|
11
15
|
import type { DiskText } from './host-types.ts';
|
|
12
16
|
import { messageOf } from './error-message.ts';
|
|
@@ -25,63 +29,68 @@ export interface ProjectFiles {
|
|
|
25
29
|
stage(files: readonly StagedFile[]): void;
|
|
26
30
|
/* Writes the new texts through the machine; the reason of the first file that could not be saved, or null. */
|
|
27
31
|
save(files: readonly StagedFile[]): Promise<string | null>;
|
|
28
|
-
/* 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. */
|
|
29
|
-
rename(from: string, to: string): Promise<string | null>;
|
|
30
|
-
/* Makes a file
|
|
31
|
-
create?(path: string, text: string): Promise<string | null>;
|
|
32
|
+
/* 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. Without `exists` an edit cannot tell whether the target is there, so refuse a target that is. With `overwrite` the target is there and is replaced, which needs `supportsOverwrite`; what is open on the replaced target is the host's to close or reload. */
|
|
33
|
+
rename(from: string, to: string, options?: { readonly overwrite: boolean }): Promise<string | null>;
|
|
34
|
+
/* Makes a file with its text, through the machine, and tells the servers it was made; the reason it could not, or null. Without it no edit creates a file. Without `exists`, refuse a file that is there. With `overwrite` the file is there and its content becomes `text`, which needs `supportsOverwrite`; an editor still open on it is set to `text` by the edit itself. */
|
|
35
|
+
create?(path: string, text: string, options?: { readonly overwrite: boolean }): Promise<string | null>;
|
|
36
|
+
/* Deletes a file, or a folder with what is in it when `recursive`, and tells the servers it is gone; the reason it could not, or null. Without it no edit deletes anything. */
|
|
37
|
+
delete?(path: string, options: { readonly recursive: boolean }): Promise<string | null>;
|
|
38
|
+
/* Whether `create` and `rename` take `overwrite`. Without it an edit that asks to overwrite a file is refused, since the file is there. */
|
|
39
|
+
readonly supportsOverwrite?: boolean;
|
|
40
|
+
/* Whether a draft or an open editor at the path, or under it for a folder, holds text that is not saved. A delete that would lose it is refused. */
|
|
41
|
+
isUnsaved?(path: string): boolean;
|
|
42
|
+
/* Whether anything is at the path: a file of any kind or a folder. Without it, what is no text file is of unknown existence: an edit may move or delete it and the machine decides, and `ignoreIfNotExists` cannot be honored. */
|
|
43
|
+
exists?(path: string): Promise<boolean>;
|
|
44
|
+
/* Whether the path is a folder. Without it, deleting anything that is no text file needs `recursive`. */
|
|
45
|
+
isFolder?(path: string): Promise<boolean>;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
export interface WorkspaceEditHost {
|
|
35
49
|
/* The editor whose document this is, which takes the edit as one undo step. */
|
|
36
50
|
editorOf(uri: string): Editor | undefined;
|
|
37
51
|
readonly files: ProjectFiles | null;
|
|
52
|
+
/* The version the language servers hold an open document at; null when it is not known. The version of an edit is only enforced for an open document when this reports one; the text check stands in otherwise. */
|
|
53
|
+
versionOf?(uri: string): number | null;
|
|
54
|
+
/* Whether the editor of a document is read only. Without it the editor's own `isReadOnly` says, and an editor without that is found out when the edit is applied. */
|
|
55
|
+
isReadOnly?(uri: string): boolean;
|
|
56
|
+
/* The URIs of every document an editor of this project holds. With it, deleting a folder finds open documents under it when `files.isUnsaved` is missing; without both, a recursive delete is refused. */
|
|
57
|
+
openUris?(): readonly string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* One resource operation of an edit, for a preview: a file that is made, moved or deleted. `newUri` is where a move ends. */
|
|
61
|
+
export interface ResourceOperationView {
|
|
62
|
+
readonly kind: ResourceOperationKind;
|
|
63
|
+
readonly uri: string;
|
|
64
|
+
readonly newUri?: string;
|
|
38
65
|
}
|
|
39
66
|
|
|
40
67
|
function refused(failureReason: string): ApplyWorkspaceEditResult {
|
|
41
68
|
return { applied: false, failureReason };
|
|
42
69
|
}
|
|
43
70
|
|
|
44
|
-
/*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/* The steps of an edit; null when it deletes a file, which an edit is not applied with. */
|
|
51
|
-
function stepsOf(edit: WorkspaceEdit): Step[] | null {
|
|
52
|
-
const steps: Step[] = [];
|
|
53
|
-
for (const change of edit.documentChanges ?? []) {
|
|
54
|
-
if ('textDocument' in change) {
|
|
55
|
-
steps.push({ kind: 'edits', uri: change.textDocument.uri, edits: change.edits });
|
|
56
|
-
} else if (change.kind === 'create') {
|
|
57
|
-
steps.push({ kind: 'create', file: change });
|
|
58
|
-
} else if (change.kind === 'rename') {
|
|
59
|
-
steps.push({ kind: 'rename', oldUri: change.oldUri, newUri: change.newUri });
|
|
60
|
-
} else {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
71
|
+
/* The text edits of an edit per document, as the entries a document has in it, whatever else the edit does. */
|
|
72
|
+
export function textEntriesOf(edit: WorkspaceEdit): Map<string, TextEdit[][]> {
|
|
73
|
+
const entries = new Map<string, TextEdit[][]>();
|
|
74
|
+
const add = (uri: string, edits: TextEdit[]): void => {
|
|
75
|
+
entries.set(uri, [...(entries.get(uri) ?? []), edits]);
|
|
76
|
+
};
|
|
64
77
|
if (edit.documentChanges === undefined) {
|
|
65
78
|
for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
|
|
66
|
-
|
|
79
|
+
add(uri, edits);
|
|
80
|
+
}
|
|
81
|
+
return entries;
|
|
82
|
+
}
|
|
83
|
+
for (const change of edit.documentChanges) {
|
|
84
|
+
if ('textDocument' in change) {
|
|
85
|
+
add(change.textDocument.uri, change.edits);
|
|
67
86
|
}
|
|
68
87
|
}
|
|
69
|
-
return
|
|
88
|
+
return entries;
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
/* The text edits of an edit per document, as the entries a document has in it; null when it deletes a file. */
|
|
73
92
|
export function entriesOf(edit: WorkspaceEdit): Map<string, TextEdit[][]> | null {
|
|
74
|
-
|
|
75
|
-
if (steps === null) {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const entries = new Map<string, TextEdit[][]>();
|
|
79
|
-
for (const step of steps) {
|
|
80
|
-
if (step.kind === 'edits') {
|
|
81
|
-
entries.set(step.uri, [...(entries.get(step.uri) ?? []), step.edits]);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return entries;
|
|
93
|
+
return (edit.documentChanges ?? []).some((change) => 'kind' in change && change.kind === 'delete') ? null : textEntriesOf(edit);
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/* The files an edit moves, which a preview of lines cannot show. */
|
|
@@ -89,165 +98,334 @@ export function renamesOf(edit: WorkspaceEdit): RenameFile[] {
|
|
|
89
98
|
return (edit.documentChanges ?? []).filter((change): change is RenameFile => 'kind' in change && change.kind === 'rename');
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
/* Every file an edit makes, moves or deletes, in the order it says. */
|
|
102
|
+
export function resourceOperationsOf(edit: WorkspaceEdit): ResourceOperationView[] {
|
|
103
|
+
const operations: ResourceOperationView[] = [];
|
|
104
|
+
for (const change of edit.documentChanges ?? []) {
|
|
105
|
+
if ('textDocument' in change) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
operations.push(change.kind === 'rename' ? { kind: 'rename', uri: change.oldUri, newUri: change.newUri } : { kind: change.kind, uri: change.uri });
|
|
109
|
+
}
|
|
110
|
+
return operations;
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
function contentChangeOf(edit: TextEdit): EditorContentChange {
|
|
93
114
|
return { range: edit.range, text: edit.newText };
|
|
94
115
|
}
|
|
95
116
|
|
|
96
|
-
/*
|
|
97
|
-
interface
|
|
98
|
-
|
|
99
|
-
readonly
|
|
100
|
-
|
|
101
|
-
readonly
|
|
102
|
-
readonly after: string;
|
|
103
|
-
/* Set when no editor holds the document: what the machine has, which the new text is a draft of. */
|
|
104
|
-
readonly disk: DiskText | null;
|
|
117
|
+
/* Everything the plan was made from that has to be true still when it runs. */
|
|
118
|
+
interface Reads {
|
|
119
|
+
/* The text an open editor had when it was read, by the URI it was read at. */
|
|
120
|
+
readonly open: Map<string, { readonly text: string; readonly version: number | null }>;
|
|
121
|
+
/* What the machine had of a file no editor held, by the URI it was read at. */
|
|
122
|
+
readonly disks: Map<string, DiskText>;
|
|
105
123
|
}
|
|
106
124
|
|
|
107
|
-
/*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
/* 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. */
|
|
126
|
+
export function supportedResourceOperations(files: ProjectFiles | null | undefined): ResourceOperationKind[] {
|
|
127
|
+
const operations: ResourceOperationKind[] = [];
|
|
128
|
+
if (files?.create !== undefined) {
|
|
129
|
+
operations.push('create');
|
|
130
|
+
}
|
|
131
|
+
if (files?.rename !== undefined) {
|
|
132
|
+
operations.push('rename');
|
|
133
|
+
}
|
|
134
|
+
if (files?.delete !== undefined) {
|
|
135
|
+
operations.push('delete');
|
|
136
|
+
}
|
|
137
|
+
return operations;
|
|
112
138
|
}
|
|
113
139
|
|
|
114
|
-
|
|
140
|
+
function within(uri: string, folder: string): boolean {
|
|
141
|
+
return uri.startsWith(`${folder}/`);
|
|
142
|
+
}
|
|
115
143
|
|
|
116
|
-
/*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
const planned = await plan(steps, host);
|
|
128
|
-
if (typeof planned === 'string') {
|
|
129
|
-
return refused(planned);
|
|
130
|
-
}
|
|
131
|
-
const moving = planned.some((item) => item.kind === 'rename');
|
|
132
|
-
const staged: StagedFile[] = [];
|
|
133
|
-
for (const item of planned) {
|
|
134
|
-
if (item.kind === 'rename' || item.kind === 'create') {
|
|
135
|
-
const reason = item.kind === 'rename' ? await host.files!.rename(item.oldPath, item.newPath) : await host.files!.create!(item.path, item.text);
|
|
136
|
-
if (reason !== null) {
|
|
137
|
-
return refused(reason);
|
|
144
|
+
/* 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. */
|
|
145
|
+
function placesOf(uri: string, renames: readonly RenameFile[]): string[] {
|
|
146
|
+
const found = new Set([uri]);
|
|
147
|
+
for (let i = renames.length - 1; i >= 0; i--) {
|
|
148
|
+
const { oldUri, newUri } = renames[i]!;
|
|
149
|
+
for (const place of [...found]) {
|
|
150
|
+
if (place === newUri || within(place, newUri)) {
|
|
151
|
+
found.add(oldUri + place.slice(newUri.length));
|
|
152
|
+
} else if (within(newUri, place)) {
|
|
153
|
+
found.add(oldUri);
|
|
138
154
|
}
|
|
139
|
-
continue;
|
|
140
155
|
}
|
|
141
|
-
|
|
156
|
+
}
|
|
157
|
+
return [...found];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* 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. */
|
|
161
|
+
function losesWork(recursive: boolean, places: readonly string[], host: WorkspaceEditHost): boolean {
|
|
162
|
+
const files = host.files!;
|
|
163
|
+
if (files.isUnsaved !== undefined) {
|
|
164
|
+
return places.some((place) => {
|
|
165
|
+
const path = fileUriToPath(place);
|
|
166
|
+
return path !== null && files.isUnsaved!(path);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (host.openUris === undefined && recursive) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
const open = host.openUris?.() ?? [];
|
|
173
|
+
return places.some((place) => host.editorOf(place) !== undefined || open.some((uri) => uri === place || within(uri, place)));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readOnlyAt(host: WorkspaceEditHost, uri: string, editor: Editor | undefined): boolean {
|
|
177
|
+
return host.isReadOnly?.(uri) ?? editor?.isReadOnly?.() ?? false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Why a create or move over a file that is there is refused; null when the host can replace it without losing work. */
|
|
181
|
+
function overwriteProblem(
|
|
182
|
+
step: PlannedCreateStep | PlannedRenameStep,
|
|
183
|
+
target: string,
|
|
184
|
+
places: readonly string[],
|
|
185
|
+
host: WorkspaceEditHost,
|
|
186
|
+
reads: Reads
|
|
187
|
+
): string | null {
|
|
188
|
+
const path = fileUriToPath(target)!;
|
|
189
|
+
if (host.files?.supportsOverwrite !== true) {
|
|
190
|
+
return `${path} already exists`;
|
|
191
|
+
}
|
|
192
|
+
// A target no text was read from may be a folder, with unsaved work anywhere under it.
|
|
193
|
+
const isText = step.kind === 'create' || reads.open.has(target) || reads.disks.has(target);
|
|
194
|
+
if (losesWork(!isText, places, host)) {
|
|
195
|
+
return `${path} has changes that are not saved`;
|
|
196
|
+
}
|
|
197
|
+
const editor = host.editorOf(target);
|
|
198
|
+
return editor !== undefined && step.kind === 'create' && readOnlyAt(host, target, editor) ? `${path} is read only` : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Checks a plan against what only the host can see: paths, overwrites, unsaved work and the editors it touches. Null when it fits. */
|
|
202
|
+
function problemWith(steps: readonly WorkspaceEditStep[], host: WorkspaceEditHost, reads: Reads): string | null {
|
|
203
|
+
const renames: RenameFile[] = [];
|
|
204
|
+
for (const step of steps) {
|
|
205
|
+
if (step.kind === 'edits') {
|
|
206
|
+
if (step.open && readOnlyAt(host, step.readUri, host.editorOf(step.readUri))) {
|
|
207
|
+
return `${step.uri} is read only`;
|
|
208
|
+
}
|
|
142
209
|
continue;
|
|
143
210
|
}
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
211
|
+
const uris = step.kind === 'rename' ? [step.oldUri, step.newUri] : [step.uri];
|
|
212
|
+
const paths = uris.map((uri) => fileUriToPath(uri));
|
|
213
|
+
if (paths.includes(null)) {
|
|
214
|
+
return `${uris[0]} cannot be changed from here`;
|
|
215
|
+
}
|
|
216
|
+
if ((step.kind === 'create' || step.kind === 'rename') && step.overwrite) {
|
|
217
|
+
const target = step.kind === 'create' ? step.uri : step.newUri;
|
|
218
|
+
const refusal = overwriteProblem(step, target, placesOf(target, renames), host, reads);
|
|
219
|
+
if (refusal !== null) {
|
|
220
|
+
return refusal;
|
|
149
221
|
}
|
|
150
|
-
continue;
|
|
151
222
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
continue;
|
|
223
|
+
if (step.kind === 'rename') {
|
|
224
|
+
renames.push({ kind: 'rename', oldUri: step.oldUri, newUri: step.newUri });
|
|
155
225
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
226
|
+
if (step.kind === 'delete' && losesWork(step.recursive, placesOf(step.uri, renames), host)) {
|
|
227
|
+
return `${paths[0]} has changes that are not saved`;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const [uri, planned] of reads.open) {
|
|
231
|
+
const editor = host.editorOf(uri);
|
|
232
|
+
if (editor === undefined || editor.getText() !== planned.text) {
|
|
233
|
+
return `${uri} changed while the edit was being prepared`;
|
|
160
234
|
}
|
|
161
|
-
const
|
|
235
|
+
const version = host.versionOf?.(uri) ?? null;
|
|
236
|
+
if (planned.version !== null && version !== planned.version) {
|
|
237
|
+
return `${uri} changed while the edit was being prepared`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const uri of reads.disks.keys()) {
|
|
241
|
+
if (host.editorOf(uri) !== undefined) {
|
|
242
|
+
return `${uri} was opened while the edit was being prepared`;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
* Applies an LSP workspace edit step by step, after planning it against every text (`planWorkspaceOperations`)
|
|
250
|
+
* so one that does not fit changes nothing. What the host supports follows `host.files`: moves need `rename`,
|
|
251
|
+
* makes need `create` and deletes need `delete`. Just before the first step runs, every open editor must still
|
|
252
|
+
* hold the text and version the plan was made from, and a delete must not lose unsaved work. Each text step
|
|
253
|
+
* and each delete checks that again right before it runs, since the steps before it are awaited.
|
|
254
|
+
*
|
|
255
|
+
* A file an editor holds takes its edits as one undo step per step of the edit; any other file gets an unsaved
|
|
256
|
+
* draft, since nothing is written to disk on a server's word alone, unless the edit also moves files: then it
|
|
257
|
+
* is one change, saved and moved through the machine. A create or a move over a file that is there is refused,
|
|
258
|
+
* unless it says `overwrite` and `files.supportsOverwrite` is set. Then it is refused only when the target, or
|
|
259
|
+
* anything under a folder target, has unsaved work, checked again right before the step runs. `overwrite` wins
|
|
260
|
+
* over `ignoreIfExists`, as the protocol says. A created file that an editor still holds shows its new text.
|
|
261
|
+
* The steps run in order and a step that no longer fits stops the edit there, with the steps before it applied
|
|
262
|
+
* and nothing rolled back. A folder that only exists because an earlier move in the same edit made it is
|
|
263
|
+
* looked for where it was and refused.
|
|
264
|
+
*/
|
|
265
|
+
export async function applyWorkspaceEdit(edit: WorkspaceEdit, host: WorkspaceEditHost): Promise<ApplyWorkspaceEditResult> {
|
|
266
|
+
const reads: Reads = { open: new Map(), disks: new Map() };
|
|
267
|
+
const files = host.files;
|
|
268
|
+
let steps: readonly WorkspaceEditStep[];
|
|
269
|
+
try {
|
|
270
|
+
({ steps } = await planWorkspaceOperations(edit, {
|
|
271
|
+
operations: supportedResourceOperations(files),
|
|
272
|
+
read: async (uri) => {
|
|
273
|
+
const editor = host.editorOf(uri);
|
|
274
|
+
if (editor !== undefined) {
|
|
275
|
+
const text = editor.getText();
|
|
276
|
+
const version = host.versionOf?.(uri) ?? null;
|
|
277
|
+
reads.open.set(uri, { text, version });
|
|
278
|
+
return { text, version, open: true };
|
|
279
|
+
}
|
|
280
|
+
const path = fileUriToPath(uri);
|
|
281
|
+
const disk = path === null || files === null ? null : await files.read(path);
|
|
282
|
+
if (disk === null) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
reads.disks.set(uri, disk);
|
|
286
|
+
return { text: disk.text, version: null, open: false };
|
|
287
|
+
},
|
|
288
|
+
...(files?.exists === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
exists: async (uri: string) => {
|
|
292
|
+
const path = fileUriToPath(uri);
|
|
293
|
+
return path !== null && (await files.exists!(path));
|
|
294
|
+
}
|
|
295
|
+
}),
|
|
296
|
+
...(files?.isFolder === undefined
|
|
297
|
+
? {}
|
|
298
|
+
: {
|
|
299
|
+
isFolder: async (uri: string) => {
|
|
300
|
+
const path = fileUriToPath(uri);
|
|
301
|
+
return path === null || (await files.isFolder!(path));
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
}));
|
|
305
|
+
} catch (error) {
|
|
306
|
+
return refused(messageOf(error));
|
|
307
|
+
}
|
|
308
|
+
const problem = problemWith(steps, host, reads);
|
|
309
|
+
if (problem !== null) {
|
|
310
|
+
return refused(problem);
|
|
311
|
+
}
|
|
312
|
+
const running: Running = { ahead: [], reads, saving: steps.some((step) => step.kind === 'rename'), staged: [], touched: new Set() };
|
|
313
|
+
for (const [index, step] of steps.entries()) {
|
|
314
|
+
running.ahead = steps.slice(index + 1);
|
|
315
|
+
const reason = await run(step, host, running);
|
|
162
316
|
if (reason !== null) {
|
|
163
317
|
return refused(reason);
|
|
164
318
|
}
|
|
165
319
|
}
|
|
166
|
-
if (staged.length > 0) {
|
|
167
|
-
|
|
320
|
+
if (running.staged.length > 0) {
|
|
321
|
+
files?.stage(running.staged);
|
|
168
322
|
}
|
|
169
323
|
return { applied: true };
|
|
170
324
|
}
|
|
171
325
|
|
|
172
|
-
/*
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
created.text = applyTextEdits(created.text, step.edits);
|
|
207
|
-
} catch (error) {
|
|
208
|
-
return messageOf(error);
|
|
209
|
-
}
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (!texts.has(source)) {
|
|
213
|
-
const editor = host.editorOf(source);
|
|
214
|
-
if (editor) {
|
|
215
|
-
texts.set(source, { text: editor.getText(), disk: null });
|
|
216
|
-
} else {
|
|
217
|
-
const path = fileUriToPath(source);
|
|
218
|
-
const disk = path === null || host.files === null ? null : await host.files.read(path);
|
|
219
|
-
if (disk === null) {
|
|
220
|
-
return `${source} cannot be read as text`;
|
|
221
|
-
}
|
|
222
|
-
texts.set(source, { text: disk.text, disk });
|
|
223
|
-
}
|
|
326
|
+
/* What the steps share while they run. */
|
|
327
|
+
interface Running {
|
|
328
|
+
/* The steps still to run after the current one. */
|
|
329
|
+
ahead: readonly WorkspaceEditStep[];
|
|
330
|
+
readonly reads: Reads;
|
|
331
|
+
/* A rename is in the edit, so the files it edits are saved in order instead of staged as drafts. */
|
|
332
|
+
readonly saving: boolean;
|
|
333
|
+
readonly staged: StagedFile[];
|
|
334
|
+
/* Editors this run edited, whose version moved because of it. */
|
|
335
|
+
readonly touched: Set<Editor>;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* Why a text step no longer fits its editor, now that the steps before it ran; null when it does. */
|
|
339
|
+
function driftOf(step: PlannedTextStep, host: WorkspaceEditHost, uri: string, editor: Editor, touched: ReadonlySet<Editor>): string | null {
|
|
340
|
+
if (editor.getText() !== step.before) {
|
|
341
|
+
return `${step.uri} changed while the edit was being applied`;
|
|
342
|
+
}
|
|
343
|
+
const version = uri === step.readUri && !touched.has(editor) ? host.versionOf?.(uri) : undefined;
|
|
344
|
+
if (version !== undefined && step.version !== null && version !== step.version) {
|
|
345
|
+
return `${step.uri} changed while the edit was being applied`;
|
|
346
|
+
}
|
|
347
|
+
return readOnlyAt(host, uri, editor) ? `${step.uri} is read only` : null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* Does one step; the reason it could not, or null. */
|
|
351
|
+
async function run(step: WorkspaceEditStep, host: WorkspaceEditHost, running: Running): Promise<string | null> {
|
|
352
|
+
const files = host.files!;
|
|
353
|
+
if (step.kind === 'create') {
|
|
354
|
+
return step.overwrite ? overwriteFile(step, host) : files.create!(fileUriToPath(step.uri)!, step.text);
|
|
355
|
+
}
|
|
356
|
+
if (step.kind === 'rename') {
|
|
357
|
+
const [from, to] = [fileUriToPath(step.oldUri)!, fileUriToPath(step.newUri)!];
|
|
358
|
+
if (!step.overwrite) {
|
|
359
|
+
return files.rename(from, to);
|
|
224
360
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
361
|
+
// The steps before this one ran, so the unsaved work at the target is looked for once more.
|
|
362
|
+
return losesWork(!running.reads.disks.has(step.newUri) && !running.reads.open.has(step.newUri), [step.newUri], host)
|
|
363
|
+
? `${to} has changes that are not saved`
|
|
364
|
+
: files.rename(from, to, { overwrite: true });
|
|
365
|
+
}
|
|
366
|
+
if (step.kind === 'delete') {
|
|
367
|
+
const path = fileUriToPath(step.uri)!;
|
|
368
|
+
if (losesWork(step.recursive, [step.uri], host)) {
|
|
369
|
+
return `${path} has changes that are not saved`;
|
|
231
370
|
}
|
|
232
|
-
|
|
233
|
-
|
|
371
|
+
return files.delete!(path, { recursive: step.recursive });
|
|
372
|
+
}
|
|
373
|
+
if (step.after === step.before) {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
if (step.open) {
|
|
377
|
+
const uri = host.editorOf(step.uri) === undefined ? step.readUri : step.uri;
|
|
378
|
+
return editOpen(step, host, running, uri, host.editorOf(uri));
|
|
234
379
|
}
|
|
235
|
-
return
|
|
380
|
+
return runDraft(step, host, running);
|
|
236
381
|
}
|
|
237
382
|
|
|
238
|
-
/*
|
|
239
|
-
async function
|
|
240
|
-
|
|
241
|
-
host
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
246
|
-
|
|
383
|
+
/* Empties a file that is there and gives it its text; the editor that still holds it is set to the same text. */
|
|
384
|
+
async function overwriteFile(step: PlannedCreateStep, host: WorkspaceEditHost): Promise<string | null> {
|
|
385
|
+
const path = fileUriToPath(step.uri)!;
|
|
386
|
+
if (losesWork(false, [step.uri], host)) {
|
|
387
|
+
return `${path} has changes that are not saved`;
|
|
388
|
+
}
|
|
389
|
+
const reason = await host.files!.create!(path, step.text, { overwrite: true });
|
|
390
|
+
if (reason === null) {
|
|
391
|
+
host.editorOf(step.uri)?.setText(step.text);
|
|
392
|
+
}
|
|
393
|
+
return reason;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function editOpen(step: PlannedTextStep, host: WorkspaceEditHost, running: Running, uri: string, editor: Editor | undefined): string | null {
|
|
397
|
+
if (editor === undefined) {
|
|
398
|
+
return `${step.uri} is not open any more`;
|
|
399
|
+
}
|
|
400
|
+
const drift = driftOf(step, host, uri, editor, running.touched);
|
|
401
|
+
if (drift !== null) {
|
|
402
|
+
return drift;
|
|
403
|
+
}
|
|
404
|
+
running.touched.add(editor);
|
|
405
|
+
// The edits stay apart, so the caret and what is drawn over the text stay where they were between them.
|
|
406
|
+
return editor.applyEdits(step.edits.map(contentChangeOf)) ? null : `${step.uri} is read only`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* 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. */
|
|
410
|
+
function deletedAhead(uri: string, ahead: readonly WorkspaceEditStep[]): boolean {
|
|
411
|
+
return ahead.some((step) => step.kind === 'delete' && (step.uri === uri || within(uri, step.uri.replace(/\/$/, ''))));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function runDraft(step: PlannedTextStep, host: WorkspaceEditHost, running: Running): Promise<string | null> {
|
|
415
|
+
const { reads, saving, staged, ahead } = running;
|
|
416
|
+
const uri = [step.uri, step.readUri].find((candidate) => host.editorOf(candidate) !== undefined);
|
|
417
|
+
if (uri !== undefined) {
|
|
418
|
+
return editOpen(step, host, running, uri, host.editorOf(uri));
|
|
247
419
|
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
420
|
+
const path = fileUriToPath(step.uri);
|
|
421
|
+
const disk = reads.disks.get(step.readUri);
|
|
422
|
+
if (path === null || disk === undefined || deletedAhead(step.uri, ahead)) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const file = { path, disk, text: step.after };
|
|
426
|
+
if (!saving) {
|
|
427
|
+
staged.push(file);
|
|
428
|
+
return null;
|
|
251
429
|
}
|
|
252
|
-
return
|
|
430
|
+
return host.files!.save([file]);
|
|
253
431
|
}
|