@adecore/editor-react 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CallHierarchyPanel.d.ts +6 -0
- package/dist/CallHierarchyPanel.js +88 -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/PeekPanel.d.ts +3 -0
- package/dist/PeekPanel.js +2 -3
- 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 +99 -0
- package/dist/call-hierarchy.d.ts +42 -0
- package/dist/call-hierarchy.js +399 -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 +14 -4
- 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 +17 -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 +114 -3
- package/dist/locales/nl.json +114 -3
- package/dist/models.d.ts +6 -2
- package/dist/models.js +5 -1
- 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.js +1 -0
- 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 +8 -0
- package/dist/project-language.js +34 -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 +62 -0
- package/dist/rendered-docs.js +368 -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/workspace-edit.d.ts +25 -3
- package/dist/workspace-edit.js +282 -143
- package/package.json +10 -5
- package/src/CallHierarchyPanel.tsx +238 -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/PeekPanel.tsx +2 -3
- 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 +158 -0
- package/src/call-hierarchy.ts +441 -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 +21 -5
- package/src/editor-react.css +79 -0
- package/src/format-on-type.ts +111 -0
- package/src/host-types.ts +19 -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 +114 -3
- package/src/locales/nl.json +114 -3
- package/src/models.ts +40 -1
- package/src/patch-decorations.ts +224 -0
- package/src/patch-layout.ts +388 -0
- package/src/peek.ts +1 -0
- package/src/pick.ts +12 -1
- package/src/popups.ts +24 -0
- package/src/project-language.ts +43 -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 +422 -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/workspace-edit.ts +347 -169
package/dist/symbols.js
CHANGED
|
@@ -19,7 +19,7 @@ const KINDS = {
|
|
|
19
19
|
};
|
|
20
20
|
function walk(symbols, spans) {
|
|
21
21
|
for (const symbol of symbols) {
|
|
22
|
-
spans.push({ name: symbol.name, kind: symbol.kind, range: symbol.range });
|
|
22
|
+
spans.push({ name: symbol.name, kind: symbol.kind, range: symbol.range, nameLine: symbol.selectionRange?.start.line });
|
|
23
23
|
walk(symbol.children ?? [], spans);
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -35,16 +35,48 @@ export function symbolSpansOf(result) {
|
|
|
35
35
|
}
|
|
36
36
|
return result.map((symbol) => ({ name: symbol.name, kind: symbol.kind, range: symbol.location.range }));
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
/* A line a declaration can have above it: blank, a comment, or an attribute. A bare `#` is no comment, since it heads a Markdown section. */
|
|
39
|
+
const PRELUDE = /^\s*(?:$|\/\/|\/\*|\*|#\[)/;
|
|
40
|
+
/*
|
|
41
|
+
* The line a block is pinned by. A range may begin at the doc comment or the attributes of a declaration, so the
|
|
42
|
+
* line of the name wins, and without it the first line below the comment that holds the name.
|
|
43
|
+
*/
|
|
44
|
+
function headerLineOf({ name, range, nameLine }, lines) {
|
|
45
|
+
const { start, end } = range;
|
|
46
|
+
if (nameLine !== undefined && nameLine >= start.line && nameLine <= end.line) {
|
|
47
|
+
return nameLine;
|
|
48
|
+
}
|
|
49
|
+
if (lines === undefined) {
|
|
50
|
+
return start.line;
|
|
51
|
+
}
|
|
52
|
+
const last = Math.min(end.line, lines.length - 1);
|
|
53
|
+
let first = start.line;
|
|
54
|
+
while (first < last && PRELUDE.test(lines[first] ?? '')) {
|
|
55
|
+
first++;
|
|
56
|
+
}
|
|
57
|
+
const short = name.slice(name.lastIndexOf('\\') + 1);
|
|
58
|
+
for (let line = first; line <= last; line++) {
|
|
59
|
+
if (!PRELUDE.test(lines[line] ?? '') && (lines[line] ?? '').includes(short)) {
|
|
60
|
+
return line;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return first;
|
|
64
|
+
}
|
|
65
|
+
function blockOf(span, lines) {
|
|
66
|
+
const { name, kind, range } = span;
|
|
39
67
|
const label = KINDS[kind];
|
|
40
68
|
// A one-line symbol has no body to pin, and a property or an enum member is part of the block around it.
|
|
41
69
|
return label === undefined || range.end.line <= range.start.line
|
|
42
70
|
? null
|
|
43
|
-
: { startLine:
|
|
71
|
+
: { startLine: headerLineOf(span, lines) + 1, endLine: range.end.line + 1, name, kind: label };
|
|
44
72
|
}
|
|
45
|
-
/*
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
/*
|
|
74
|
+
* The multi-line symbols of a file as blocks, outermost first, in one-based lines. The text of the document
|
|
75
|
+
* lets a block start at its declaration rather than at the comment a server counts into the symbol.
|
|
76
|
+
*/
|
|
77
|
+
export function blocksOf(result, text) {
|
|
78
|
+
const lines = text?.split('\n');
|
|
79
|
+
const blocks = symbolSpansOf(result).flatMap((span) => blockOf(span, lines) ?? []);
|
|
48
80
|
return blocks.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
|
|
49
81
|
}
|
|
50
82
|
/*
|
|
@@ -62,7 +94,7 @@ export class SymbolsFeature {
|
|
|
62
94
|
}
|
|
63
95
|
const result = await project.service.documentSymbols(uri, { signal });
|
|
64
96
|
if (!signal.aborted) {
|
|
65
|
-
editor.setBlocks(result === null ? null : blocksOf(result));
|
|
97
|
+
editor.setBlocks(result === null ? null : blocksOf(result, editor.getText()));
|
|
66
98
|
for (const listener of [...this.listeners]) {
|
|
67
99
|
listener(result);
|
|
68
100
|
}
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LanguageService, type LanguageDocument, type LanguageRequestOptions, type DiagnosticsReport, type ContentChange, type Disposable, type ProviderOptions, type CodeAction, type CodeActionContext, type CodeLens, type Command, type CompletionContext, type CompletionItem, type CompletionResult, type DocumentHighlight, type DocumentSymbolResult, type FoldingRange, type FormattingOptions, type Hover, type InlayHint, type Location, type NavigationResult, type Position, type PrepareRenameResult, type Range, type SelectionRange, type SemanticTokens, type SemanticTokensDelta, type SignatureHelp, type SignatureHelpContext, type TextEdit, type WorkspaceEdit, type WorkspaceSymbolResult } from '@adecore/lsp';
|
|
1
|
+
import { type LanguageService, type LanguageDocument, type LanguageRequestOptions, type DiagnosticsReport, type ContentChange, type Disposable, type ProviderOptions, type CallHierarchyIncomingCall, type CallHierarchyItem, type CallHierarchyOutgoingCall, type CodeAction, type CodeActionContext, type CodeLens, type Command, type CompletionContext, type CompletionItem, type CompletionResult, type DocumentHighlight, type DocumentSymbolResult, type FoldingRange, type FormattingOptions, type Hover, type InlayHint, type Location, type NavigationResult, type Position, type PrepareRenameResult, type Range, type SelectionRange, type SemanticTokens, type SemanticTokensDelta, type SignatureHelp, type SignatureHelpContext, type TextEdit, type WorkspaceEdit, type WorkspaceSymbolResult } from '@adecore/lsp';
|
|
2
2
|
export { ManualTimers } from './timers.ts';
|
|
3
3
|
export interface LanguageCall {
|
|
4
4
|
method: string;
|
|
@@ -44,6 +44,7 @@ export declare class FakeLanguageService implements LanguageService {
|
|
|
44
44
|
executeCommand(uri: string, command: Command, options?: LanguageRequestOptions): Promise<unknown>;
|
|
45
45
|
formatting(uri: string, formatting: FormattingOptions, options?: LanguageRequestOptions): Promise<TextEdit[] | null>;
|
|
46
46
|
rangeFormatting(uri: string, range: Range, formatting: FormattingOptions, options?: LanguageRequestOptions): Promise<TextEdit[] | null>;
|
|
47
|
+
onTypeFormatting(uri: string, position: Position, ch: string, formatting: FormattingOptions, options?: LanguageRequestOptions): Promise<TextEdit[] | null>;
|
|
47
48
|
semanticTokens(uri: string, options?: LanguageRequestOptions): Promise<SemanticTokens | null>;
|
|
48
49
|
semanticTokensDelta(uri: string, previousResultId: string, options?: LanguageRequestOptions): Promise<SemanticTokens | SemanticTokensDelta | null>;
|
|
49
50
|
semanticTokensRange(uri: string, range: Range, options?: LanguageRequestOptions): Promise<SemanticTokens | null>;
|
|
@@ -52,6 +53,9 @@ export declare class FakeLanguageService implements LanguageService {
|
|
|
52
53
|
workspaceSymbols(uri: string, query: string, options?: LanguageRequestOptions): Promise<WorkspaceSymbolResult>;
|
|
53
54
|
foldingRanges(uri: string, options?: LanguageRequestOptions): Promise<FoldingRange[] | null>;
|
|
54
55
|
selectionRanges(uri: string, positions: readonly Position[], options?: LanguageRequestOptions): Promise<SelectionRange[] | null>;
|
|
56
|
+
prepareCallHierarchy(uri: string, position: Position, options?: LanguageRequestOptions): Promise<CallHierarchyItem[] | null>;
|
|
57
|
+
incomingCalls(uri: string, item: CallHierarchyItem, options?: LanguageRequestOptions): Promise<CallHierarchyIncomingCall[] | null>;
|
|
58
|
+
outgoingCalls(uri: string, item: CallHierarchyItem, options?: LanguageRequestOptions): Promise<CallHierarchyOutgoingCall[] | null>;
|
|
55
59
|
codeLenses(uri: string, options?: LanguageRequestOptions): Promise<CodeLens[] | null>;
|
|
56
60
|
resolveCodeLens(uri: string, lens: CodeLens, options?: LanguageRequestOptions): Promise<CodeLens>;
|
|
57
61
|
}
|
package/dist/testing.js
CHANGED
|
@@ -123,6 +123,9 @@ export class FakeLanguageService {
|
|
|
123
123
|
rangeFormatting(uri, range, formatting, options = {}) {
|
|
124
124
|
return this.request('textDocument/rangeFormatting', uri, { range, options: formatting }, options);
|
|
125
125
|
}
|
|
126
|
+
onTypeFormatting(uri, position, ch, formatting, options = {}) {
|
|
127
|
+
return this.request('textDocument/onTypeFormatting', uri, { position, ch, options: formatting }, options);
|
|
128
|
+
}
|
|
126
129
|
semanticTokens(uri, options = {}) {
|
|
127
130
|
return this.request('textDocument/semanticTokens/full', uri, {}, options);
|
|
128
131
|
}
|
|
@@ -147,6 +150,15 @@ export class FakeLanguageService {
|
|
|
147
150
|
selectionRanges(uri, positions, options = {}) {
|
|
148
151
|
return this.request('textDocument/selectionRange', uri, { positions }, options);
|
|
149
152
|
}
|
|
153
|
+
prepareCallHierarchy(uri, position, options = {}) {
|
|
154
|
+
return this.request('textDocument/prepareCallHierarchy', uri, { position }, options);
|
|
155
|
+
}
|
|
156
|
+
incomingCalls(uri, item, options = {}) {
|
|
157
|
+
return this.request('callHierarchy/incomingCalls', uri, { item }, options);
|
|
158
|
+
}
|
|
159
|
+
outgoingCalls(uri, item, options = {}) {
|
|
160
|
+
return this.request('callHierarchy/outgoingCalls', uri, { item }, options);
|
|
161
|
+
}
|
|
150
162
|
codeLenses(uri, options = {}) {
|
|
151
163
|
return this.request('textDocument/codeLens', uri, {}, options);
|
|
152
164
|
}
|
package/dist/workspace-edit.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Editor } from '@adecore/editor';
|
|
2
|
-
import { type ApplyWorkspaceEditResult, type RenameFile, type TextEdit, type WorkspaceEdit } from '@adecore/lsp';
|
|
2
|
+
import { type ApplyWorkspaceEditResult, type ResourceOperationKind, type RenameFile, type TextEdit, type WorkspaceEdit } from '@adecore/lsp';
|
|
3
3
|
import type { DiskText } from './host-types.ts';
|
|
4
4
|
export interface StagedFile {
|
|
5
5
|
readonly path: string;
|
|
@@ -10,13 +10,35 @@ export interface ProjectFiles {
|
|
|
10
10
|
read(path: string): Promise<DiskText | null>;
|
|
11
11
|
stage(files: readonly StagedFile[]): void;
|
|
12
12
|
save(files: readonly StagedFile[]): Promise<string | null>;
|
|
13
|
-
rename(from: string, to: string
|
|
14
|
-
|
|
13
|
+
rename(from: string, to: string, options?: {
|
|
14
|
+
readonly overwrite: boolean;
|
|
15
|
+
}): Promise<string | null>;
|
|
16
|
+
create?(path: string, text: string, options?: {
|
|
17
|
+
readonly overwrite: boolean;
|
|
18
|
+
}): Promise<string | null>;
|
|
19
|
+
delete?(path: string, options: {
|
|
20
|
+
readonly recursive: boolean;
|
|
21
|
+
}): Promise<string | null>;
|
|
22
|
+
readonly supportsOverwrite?: boolean;
|
|
23
|
+
isUnsaved?(path: string): boolean;
|
|
24
|
+
exists?(path: string): Promise<boolean>;
|
|
25
|
+
isFolder?(path: string): Promise<boolean>;
|
|
15
26
|
}
|
|
16
27
|
export interface WorkspaceEditHost {
|
|
17
28
|
editorOf(uri: string): Editor | undefined;
|
|
18
29
|
readonly files: ProjectFiles | null;
|
|
30
|
+
versionOf?(uri: string): number | null;
|
|
31
|
+
isReadOnly?(uri: string): boolean;
|
|
32
|
+
openUris?(): readonly string[];
|
|
19
33
|
}
|
|
34
|
+
export interface ResourceOperationView {
|
|
35
|
+
readonly kind: ResourceOperationKind;
|
|
36
|
+
readonly uri: string;
|
|
37
|
+
readonly newUri?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function textEntriesOf(edit: WorkspaceEdit): Map<string, TextEdit[][]>;
|
|
20
40
|
export declare function entriesOf(edit: WorkspaceEdit): Map<string, TextEdit[][]> | null;
|
|
21
41
|
export declare function renamesOf(edit: WorkspaceEdit): RenameFile[];
|
|
42
|
+
export declare function resourceOperationsOf(edit: WorkspaceEdit): ResourceOperationView[];
|
|
43
|
+
export declare function supportedResourceOperations(files: ProjectFiles | null | undefined): ResourceOperationKind[];
|
|
22
44
|
export declare function applyWorkspaceEdit(edit: WorkspaceEdit, host: WorkspaceEditHost): Promise<ApplyWorkspaceEditResult>;
|
package/dist/workspace-edit.js
CHANGED
|
@@ -1,185 +1,324 @@
|
|
|
1
|
-
import {
|
|
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
|
|
7
|
-
function
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
+
add(uri, edits);
|
|
26
15
|
}
|
|
16
|
+
return entries;
|
|
27
17
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 === '
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
193
|
+
return null;
|
|
157
194
|
}
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
texts.set(source, { text: after, disk: held.disk });
|
|
268
|
+
return files.delete(path, { recursive: step.recursive });
|
|
171
269
|
}
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
178
|
-
return
|
|
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
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
323
|
+
return host.files.save([file]);
|
|
185
324
|
}
|