@valbuild/language-server 0.123.2 → 0.124.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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @valbuild/language-server
|
|
2
2
|
|
|
3
|
+
## 0.124.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [[`ad7fff4`](https://github.com/valbuild/val/commit/ad7fff4cc8cea98c506cf7ff9b4e8d6e5ffa4055), [`ad7fff4`](https://github.com/valbuild/val/commit/ad7fff4cc8cea98c506cf7ff9b4e8d6e5ffa4055), [`aa89fc2`](https://github.com/valbuild/val/commit/aa89fc26de7028b3c5a1666b99afc03b39e92769)]:
|
|
8
|
+
- @valbuild/server@0.124.0
|
|
9
|
+
- @valbuild/shared@0.124.0
|
|
10
|
+
- @valbuild/core@0.124.0
|
|
11
|
+
|
|
12
|
+
## 0.123.3
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- [#623](https://github.com/valbuild/val/pull/623) [`4c9369b`](https://github.com/valbuild/val/commit/4c9369bad3f17e96868262e78a4f47361d85319e) Thanks [@freekh](https://github.com/freekh)! - Add the editor quick fix for a `.jsonValues()` entry written inline: **Val: move entry into its own .val.json**.
|
|
17
|
+
|
|
18
|
+
The language server already reported the problem — "Entry '…' is written inline … Run 'val validate --fix' to move it" — but offered no way to act on it, so the only remedy was to leave the editor and run the CLI. It now offers a quick fix that creates the `*.val.json` with the entry's content and rewrites the `.val.ts` to `c.json(() => import("./…"))`, in one undoable step.
|
|
19
|
+
|
|
20
|
+
The fix is computed from the same code `val validate --fix` runs, so the two write the same files, and it is computed against the buffer you are looking at rather than what is on disk — an unsaved module can be fixed without saving first. It refuses, rather than overwriting, when a file or an unsaved buffer already occupies the target path.
|
|
21
|
+
|
|
22
|
+
Requires an editor that honours file creation inside a workspace edit; VS Code does. In an editor that does not announce it, no action is offered rather than one that would rewrite the module to import a file that never got created.
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [[`4c9369b`](https://github.com/valbuild/val/commit/4c9369bad3f17e96868262e78a4f47361d85319e), [`fb1baff`](https://github.com/valbuild/val/commit/fb1baffead5228d8a86a51af65178b88738d5a64), [`accf4f8`](https://github.com/valbuild/val/commit/accf4f852fe3400d762cc14e80316da741a69a9a), [`aef45ce`](https://github.com/valbuild/val/commit/aef45ce3ef269f1b6221de44a56df0f6a0d9dbbd)]:
|
|
25
|
+
- @valbuild/server@0.123.3
|
|
26
|
+
- @valbuild/shared@0.123.3
|
|
27
|
+
|
|
3
28
|
## 0.123.2
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
|
@@ -7,7 +7,10 @@ import type { ValModuleContent } from "./ValProject.js";
|
|
|
7
7
|
import { minimalTextEdit } from "./textEdit.js";
|
|
8
8
|
export { minimalTextEdit };
|
|
9
9
|
export declare function isLocalFix(fix: string): fix is ValidationFix;
|
|
10
|
-
export declare function
|
|
10
|
+
export declare function isWorkspaceEditFix(fix: string): fix is ValidationFix;
|
|
11
|
+
/** Whether the client will honour a file CREATE inside a `WorkspaceEdit`. */
|
|
12
|
+
export declare function canCreateFiles(capabilities: unknown): boolean;
|
|
13
|
+
export declare function createValCodeActions({ document, diagnostics, content, valRoot, moduleFilePath, read, remoteHost, allowCreateFiles, }: {
|
|
11
14
|
document: TextDocument;
|
|
12
15
|
diagnostics: Diagnostic[];
|
|
13
16
|
content: ValModuleContent;
|
|
@@ -25,6 +28,11 @@ export declare function createValCodeActions({ document, diagnostics, content, v
|
|
|
25
28
|
*/
|
|
26
29
|
read?: (fsPath: string) => string | undefined;
|
|
27
30
|
remoteHost?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the client honours a file CREATE in a `WorkspaceEdit`. Without it
|
|
33
|
+
* the extract-entry fix is not offered at all — see {@link canCreateFiles}.
|
|
34
|
+
*/
|
|
35
|
+
allowCreateFiles?: boolean;
|
|
28
36
|
}): Promise<CodeAction[]>;
|
|
29
37
|
/**
|
|
30
38
|
* Quick fix for a module that is not registered in `val.modules`.
|
|
@@ -2385,6 +2385,26 @@ function readFile(filePath) {
|
|
|
2385
2385
|
}
|
|
2386
2386
|
}
|
|
2387
2387
|
|
|
2388
|
+
/**
|
|
2389
|
+
* What the client announced it will do with a `WorkspaceEdit`.
|
|
2390
|
+
*
|
|
2391
|
+
* A resource operation — a file created, renamed or deleted as part of an edit —
|
|
2392
|
+
* is silently DROPPED by a client that did not announce it. The rest of the edit
|
|
2393
|
+
* still applies, so a fix that needs one and is offered anyway leaves the
|
|
2394
|
+
* project in a state that is worse than the one it set out to fix: a `.val.ts`
|
|
2395
|
+
* rewritten to import a file that was never created, a path rewritten to a file
|
|
2396
|
+
* that never moved. Hence: probe first, and do not offer the action at all.
|
|
2397
|
+
*
|
|
2398
|
+
* Read defensively. These come from `InitializeParams.capabilities`, which is
|
|
2399
|
+
* whatever the client sent — this is the one place that has to cope with the
|
|
2400
|
+
* field being absent or the wrong shape.
|
|
2401
|
+
*/
|
|
2402
|
+
function supportsResourceOperation(capabilities, operation) {
|
|
2403
|
+
const workspace = capabilities?.workspace;
|
|
2404
|
+
const operations = workspace?.workspaceEdit?.resourceOperations;
|
|
2405
|
+
return Array.isArray(operations) && operations.includes(operation);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2388
2408
|
/**
|
|
2389
2409
|
* Quick fixes, built by running Val's own fix machinery.
|
|
2390
2410
|
*
|
|
@@ -2408,6 +2428,18 @@ const LOCAL_FIXES = ["image:add-metadata", "image:check-metadata", "file:add-met
|
|
|
2408
2428
|
// stored metadata, dropping entries whose file has gone. Filesystem only.
|
|
2409
2429
|
"images:check-all-files", "files:check-all-files"];
|
|
2410
2430
|
|
|
2431
|
+
/**
|
|
2432
|
+
* Fixes that are local, but are NOT a patch against the document they are
|
|
2433
|
+
* reported on, so they never reach `computeFixEdit`.
|
|
2434
|
+
*
|
|
2435
|
+
* `jsonValues:extract-entry` creates a file and rewrites another — see
|
|
2436
|
+
* {@link createExtractEntryAction}. `createFixPatch` has no branch for it at all
|
|
2437
|
+
* (the fix lives in the server's own `extractJsonValuesEntry`), so routing it
|
|
2438
|
+
* through the patch pipeline would silently produce an empty patch and no
|
|
2439
|
+
* action, which is exactly what it did before this existed.
|
|
2440
|
+
*/
|
|
2441
|
+
const WORKSPACE_EDIT_FIXES = ["jsonValues:extract-entry"];
|
|
2442
|
+
|
|
2411
2443
|
/** Human-readable titles; falls back to the fix name for anything unknown. */
|
|
2412
2444
|
const FIX_TITLES = {
|
|
2413
2445
|
"image:add-metadata": "Val: add image metadata",
|
|
@@ -2415,11 +2447,20 @@ const FIX_TITLES = {
|
|
|
2415
2447
|
"file:add-metadata": "Val: add file metadata",
|
|
2416
2448
|
"file:check-metadata": "Val: update file metadata",
|
|
2417
2449
|
"images:check-all-files": "Val: update gallery image metadata",
|
|
2418
|
-
"files:check-all-files": "Val: update gallery file metadata"
|
|
2450
|
+
"files:check-all-files": "Val: update gallery file metadata",
|
|
2451
|
+
"jsonValues:extract-entry": "Val: move entry into its own .val.json"
|
|
2419
2452
|
};
|
|
2420
2453
|
function isLocalFix(fix) {
|
|
2421
2454
|
return LOCAL_FIXES.includes(fix);
|
|
2422
2455
|
}
|
|
2456
|
+
function isWorkspaceEditFix(fix) {
|
|
2457
|
+
return WORKSPACE_EDIT_FIXES.includes(fix);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/** Whether the client will honour a file CREATE inside a `WorkspaceEdit`. */
|
|
2461
|
+
function canCreateFiles(capabilities) {
|
|
2462
|
+
return supportsResourceOperation(capabilities, "create");
|
|
2463
|
+
}
|
|
2423
2464
|
|
|
2424
2465
|
/**
|
|
2425
2466
|
* Build quick fixes for the diagnostics the client sent back.
|
|
@@ -2443,7 +2484,8 @@ async function createValCodeActions({
|
|
|
2443
2484
|
valRoot,
|
|
2444
2485
|
moduleFilePath,
|
|
2445
2486
|
read,
|
|
2446
|
-
remoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST
|
|
2487
|
+
remoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST,
|
|
2488
|
+
allowCreateFiles = false
|
|
2447
2489
|
}) {
|
|
2448
2490
|
const actions = [];
|
|
2449
2491
|
/**
|
|
@@ -2492,6 +2534,26 @@ async function createValCodeActions({
|
|
|
2492
2534
|
});
|
|
2493
2535
|
continue;
|
|
2494
2536
|
}
|
|
2537
|
+
if (isWorkspaceEditFix(fix)) {
|
|
2538
|
+
if (offered.has(`${data.sourcePath}|${fix}`)) {
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
const action = createExtractEntryAction({
|
|
2542
|
+
document,
|
|
2543
|
+
sourcePath: data.sourcePath,
|
|
2544
|
+
content,
|
|
2545
|
+
valRoot,
|
|
2546
|
+
moduleFilePath,
|
|
2547
|
+
read,
|
|
2548
|
+
allowCreateFiles
|
|
2549
|
+
});
|
|
2550
|
+
if (!action) {
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
offered.add(`${data.sourcePath}|${fix}`);
|
|
2554
|
+
actions.push(action);
|
|
2555
|
+
continue;
|
|
2556
|
+
}
|
|
2495
2557
|
if (!isLocalFix(fix)) {
|
|
2496
2558
|
continue;
|
|
2497
2559
|
}
|
|
@@ -2534,6 +2596,131 @@ async function createValCodeActions({
|
|
|
2534
2596
|
}
|
|
2535
2597
|
return actions;
|
|
2536
2598
|
}
|
|
2599
|
+
|
|
2600
|
+
/**
|
|
2601
|
+
* The quick fix for `jsonValues:extract-entry`: move an entry that was written
|
|
2602
|
+
* inline in the `.val.ts` into its own `*.val.json`.
|
|
2603
|
+
*
|
|
2604
|
+
* Not a patch, and so not `computeFixEdit`. Two files change at once — a
|
|
2605
|
+
* `*.val.json` is created with the entry's content, and the `.val.ts` has the
|
|
2606
|
+
* inline value replaced by `c.json(() => import("./..."))` — and a `Patch` can
|
|
2607
|
+
* only edit one `.val.ts` and cannot create anything. That is why the fix is
|
|
2608
|
+
* `extractJsonValuesEntry` in the server rather than a `createFixPatch` branch,
|
|
2609
|
+
* and why the editor had no fix to offer for it at all until now: the patch
|
|
2610
|
+
* pipeline produced nothing and the action was dropped.
|
|
2611
|
+
*
|
|
2612
|
+
* The plan comes from the same `planJsonValuesEntryExtraction` that
|
|
2613
|
+
* `val validate --fix` runs, so the two cannot write different files; only the
|
|
2614
|
+
* last step differs. Here it is a `WorkspaceEdit`, so the change goes through
|
|
2615
|
+
* the editor's undo stack, and — crucially — is computed against the buffer the
|
|
2616
|
+
* user is looking at rather than what is on disk.
|
|
2617
|
+
*/
|
|
2618
|
+
function createExtractEntryAction({
|
|
2619
|
+
document,
|
|
2620
|
+
sourcePath,
|
|
2621
|
+
content,
|
|
2622
|
+
valRoot,
|
|
2623
|
+
moduleFilePath,
|
|
2624
|
+
read,
|
|
2625
|
+
allowCreateFiles
|
|
2626
|
+
}) {
|
|
2627
|
+
if (!moduleFilePath || !allowCreateFiles) {
|
|
2628
|
+
return undefined;
|
|
2629
|
+
}
|
|
2630
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
2631
|
+
const parts = core.Internal.splitModulePath(modulePath);
|
|
2632
|
+
// Root-only by contract, exactly as the server's fix handler asserts: the
|
|
2633
|
+
// entry is a direct child of the module's root record/router.
|
|
2634
|
+
if (parts.length !== 1) {
|
|
2635
|
+
return undefined;
|
|
2636
|
+
}
|
|
2637
|
+
const entryKey = parts[0];
|
|
2638
|
+
const entryContent = entryContentOf(content.source, entryKey);
|
|
2639
|
+
if (entryContent === undefined) {
|
|
2640
|
+
return undefined;
|
|
2641
|
+
}
|
|
2642
|
+
const planned = server.planJsonValuesEntryExtraction({
|
|
2643
|
+
moduleFilePath,
|
|
2644
|
+
entryKey,
|
|
2645
|
+
content: entryContent,
|
|
2646
|
+
valTsPath: uriToPath(document.uri),
|
|
2647
|
+
// The editor's text, not the file's: an edit computed against stale text
|
|
2648
|
+
// has ranges that land in the wrong place.
|
|
2649
|
+
valTsText: document.getText()
|
|
2650
|
+
});
|
|
2651
|
+
if (fp.result.isErr(planned)) {
|
|
2652
|
+
return undefined;
|
|
2653
|
+
}
|
|
2654
|
+
const absoluteJsonPath = path__default["default"].join(valRoot, planned.value.jsonPath);
|
|
2655
|
+
// Same refusal as the CLI's, widened by one case: an unsaved buffer counts as
|
|
2656
|
+
// occupied too. Creating over either would destroy content that a `--fix` run
|
|
2657
|
+
// refuses to touch.
|
|
2658
|
+
if (read?.(absoluteJsonPath) !== undefined || fs__default["default"].existsSync(absoluteJsonPath)) {
|
|
2659
|
+
return undefined;
|
|
2660
|
+
}
|
|
2661
|
+
const valTsEdit = minimalTextEdit(document.getText(), planned.value.valTsContent, document);
|
|
2662
|
+
if (!valTsEdit) {
|
|
2663
|
+
return undefined;
|
|
2664
|
+
}
|
|
2665
|
+
const jsonUri = pathToUri(absoluteJsonPath);
|
|
2666
|
+
return {
|
|
2667
|
+
title: FIX_TITLES["jsonValues:extract-entry"] ,
|
|
2668
|
+
kind: vscodeLanguageserver.CodeActionKind.QuickFix,
|
|
2669
|
+
edit: {
|
|
2670
|
+
// `documentChanges`, not `changes`: a plain edit map cannot create a file,
|
|
2671
|
+
// and an edit addressed to a file that does not exist is an error rather
|
|
2672
|
+
// than a creation.
|
|
2673
|
+
documentChanges: [{
|
|
2674
|
+
kind: "create",
|
|
2675
|
+
uri: jsonUri
|
|
2676
|
+
}, {
|
|
2677
|
+
textDocument: {
|
|
2678
|
+
uri: jsonUri,
|
|
2679
|
+
version: null
|
|
2680
|
+
},
|
|
2681
|
+
edits: [{
|
|
2682
|
+
range: {
|
|
2683
|
+
start: {
|
|
2684
|
+
line: 0,
|
|
2685
|
+
character: 0
|
|
2686
|
+
},
|
|
2687
|
+
end: {
|
|
2688
|
+
line: 0,
|
|
2689
|
+
character: 0
|
|
2690
|
+
}
|
|
2691
|
+
},
|
|
2692
|
+
newText: planned.value.jsonContent
|
|
2693
|
+
}]
|
|
2694
|
+
}, {
|
|
2695
|
+
textDocument: {
|
|
2696
|
+
uri: document.uri,
|
|
2697
|
+
version: null
|
|
2698
|
+
},
|
|
2699
|
+
edits: [valTsEdit]
|
|
2700
|
+
}]
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/**
|
|
2706
|
+
* One entry's value out of a module's source.
|
|
2707
|
+
*
|
|
2708
|
+
* `Object.entries` rather than an index: the source is a `Source`, which has no
|
|
2709
|
+
* index signature, and reading a dynamic key off it is the one thing this needs
|
|
2710
|
+
* to do. Mirrors what the server's fix handler reads, so the editor extracts the
|
|
2711
|
+
* same bytes `val validate --fix` would.
|
|
2712
|
+
*/
|
|
2713
|
+
function entryContentOf(source, entryKey) {
|
|
2714
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
2715
|
+
return undefined;
|
|
2716
|
+
}
|
|
2717
|
+
for (const [key, value] of Object.entries(source)) {
|
|
2718
|
+
if (key === entryKey) {
|
|
2719
|
+
return value;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return undefined;
|
|
2723
|
+
}
|
|
2537
2724
|
async function computeFixEdit({
|
|
2538
2725
|
document,
|
|
2539
2726
|
sourcePath,
|
|
@@ -3317,9 +3504,7 @@ function keysOfKeyOf(schema, snapshot) {
|
|
|
3317
3504
|
* was — worse than not offering the fix.
|
|
3318
3505
|
*/
|
|
3319
3506
|
function canRenameFiles(capabilities) {
|
|
3320
|
-
|
|
3321
|
-
const operations = workspace?.workspaceEdit?.resourceOperations;
|
|
3322
|
-
return Array.isArray(operations) && operations.includes("rename");
|
|
3507
|
+
return supportsResourceOperation(capabilities, "rename");
|
|
3323
3508
|
}
|
|
3324
3509
|
async function createGalleryMembershipActions({
|
|
3325
3510
|
document,
|
|
@@ -3792,6 +3977,7 @@ function createValLanguageServer(connection) {
|
|
|
3792
3977
|
const documents = new node.TextDocuments(vscodeLanguageserverTextdocument.TextDocument);
|
|
3793
3978
|
let canRegisterWatchers = false;
|
|
3794
3979
|
let canRenameFiles$1 = false;
|
|
3980
|
+
let canCreateFiles$1 = false;
|
|
3795
3981
|
|
|
3796
3982
|
/**
|
|
3797
3983
|
* The editor's view of a file, by absolute path, or `undefined` when the file
|
|
@@ -3955,6 +4141,10 @@ function createValLanguageServer(connection) {
|
|
|
3955
4141
|
// A RenameFile sent to a client that did not announce resourceOperations is
|
|
3956
4142
|
// silently dropped, which would rewrite the path and leave the file behind.
|
|
3957
4143
|
canRenameFiles$1 = canRenameFiles(params.capabilities);
|
|
4144
|
+
// Same reasoning for the extract-entry fix: it CREATES the `*.val.json` the
|
|
4145
|
+
// rewritten `.val.ts` imports, so a client that drops the create would be
|
|
4146
|
+
// left importing a file that is not there.
|
|
4147
|
+
canCreateFiles$1 = canCreateFiles(params.capabilities);
|
|
3958
4148
|
const clientCapabilities = params.capabilities.experimental?.val ?? {};
|
|
3959
4149
|
|
|
3960
4150
|
// Announce only what this version actually serves: a client hides UI for
|
|
@@ -4162,7 +4352,8 @@ function createValLanguageServer(connection) {
|
|
|
4162
4352
|
content: result.content,
|
|
4163
4353
|
valRoot: project.valRoot,
|
|
4164
4354
|
moduleFilePath,
|
|
4165
|
-
read: readOpenDocument
|
|
4355
|
+
read: readOpenDocument,
|
|
4356
|
+
allowCreateFiles: canCreateFiles$1
|
|
4166
4357
|
})));
|
|
4167
4358
|
return actions;
|
|
4168
4359
|
} catch (e) {
|
|
@@ -2385,6 +2385,26 @@ function readFile(filePath) {
|
|
|
2385
2385
|
}
|
|
2386
2386
|
}
|
|
2387
2387
|
|
|
2388
|
+
/**
|
|
2389
|
+
* What the client announced it will do with a `WorkspaceEdit`.
|
|
2390
|
+
*
|
|
2391
|
+
* A resource operation — a file created, renamed or deleted as part of an edit —
|
|
2392
|
+
* is silently DROPPED by a client that did not announce it. The rest of the edit
|
|
2393
|
+
* still applies, so a fix that needs one and is offered anyway leaves the
|
|
2394
|
+
* project in a state that is worse than the one it set out to fix: a `.val.ts`
|
|
2395
|
+
* rewritten to import a file that was never created, a path rewritten to a file
|
|
2396
|
+
* that never moved. Hence: probe first, and do not offer the action at all.
|
|
2397
|
+
*
|
|
2398
|
+
* Read defensively. These come from `InitializeParams.capabilities`, which is
|
|
2399
|
+
* whatever the client sent — this is the one place that has to cope with the
|
|
2400
|
+
* field being absent or the wrong shape.
|
|
2401
|
+
*/
|
|
2402
|
+
function supportsResourceOperation(capabilities, operation) {
|
|
2403
|
+
const workspace = capabilities?.workspace;
|
|
2404
|
+
const operations = workspace?.workspaceEdit?.resourceOperations;
|
|
2405
|
+
return Array.isArray(operations) && operations.includes(operation);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2388
2408
|
/**
|
|
2389
2409
|
* Quick fixes, built by running Val's own fix machinery.
|
|
2390
2410
|
*
|
|
@@ -2408,6 +2428,18 @@ const LOCAL_FIXES = ["image:add-metadata", "image:check-metadata", "file:add-met
|
|
|
2408
2428
|
// stored metadata, dropping entries whose file has gone. Filesystem only.
|
|
2409
2429
|
"images:check-all-files", "files:check-all-files"];
|
|
2410
2430
|
|
|
2431
|
+
/**
|
|
2432
|
+
* Fixes that are local, but are NOT a patch against the document they are
|
|
2433
|
+
* reported on, so they never reach `computeFixEdit`.
|
|
2434
|
+
*
|
|
2435
|
+
* `jsonValues:extract-entry` creates a file and rewrites another — see
|
|
2436
|
+
* {@link createExtractEntryAction}. `createFixPatch` has no branch for it at all
|
|
2437
|
+
* (the fix lives in the server's own `extractJsonValuesEntry`), so routing it
|
|
2438
|
+
* through the patch pipeline would silently produce an empty patch and no
|
|
2439
|
+
* action, which is exactly what it did before this existed.
|
|
2440
|
+
*/
|
|
2441
|
+
const WORKSPACE_EDIT_FIXES = ["jsonValues:extract-entry"];
|
|
2442
|
+
|
|
2411
2443
|
/** Human-readable titles; falls back to the fix name for anything unknown. */
|
|
2412
2444
|
const FIX_TITLES = {
|
|
2413
2445
|
"image:add-metadata": "Val: add image metadata",
|
|
@@ -2415,11 +2447,20 @@ const FIX_TITLES = {
|
|
|
2415
2447
|
"file:add-metadata": "Val: add file metadata",
|
|
2416
2448
|
"file:check-metadata": "Val: update file metadata",
|
|
2417
2449
|
"images:check-all-files": "Val: update gallery image metadata",
|
|
2418
|
-
"files:check-all-files": "Val: update gallery file metadata"
|
|
2450
|
+
"files:check-all-files": "Val: update gallery file metadata",
|
|
2451
|
+
"jsonValues:extract-entry": "Val: move entry into its own .val.json"
|
|
2419
2452
|
};
|
|
2420
2453
|
function isLocalFix(fix) {
|
|
2421
2454
|
return LOCAL_FIXES.includes(fix);
|
|
2422
2455
|
}
|
|
2456
|
+
function isWorkspaceEditFix(fix) {
|
|
2457
|
+
return WORKSPACE_EDIT_FIXES.includes(fix);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
/** Whether the client will honour a file CREATE inside a `WorkspaceEdit`. */
|
|
2461
|
+
function canCreateFiles(capabilities) {
|
|
2462
|
+
return supportsResourceOperation(capabilities, "create");
|
|
2463
|
+
}
|
|
2423
2464
|
|
|
2424
2465
|
/**
|
|
2425
2466
|
* Build quick fixes for the diagnostics the client sent back.
|
|
@@ -2443,7 +2484,8 @@ async function createValCodeActions({
|
|
|
2443
2484
|
valRoot,
|
|
2444
2485
|
moduleFilePath,
|
|
2445
2486
|
read,
|
|
2446
|
-
remoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST
|
|
2487
|
+
remoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST,
|
|
2488
|
+
allowCreateFiles = false
|
|
2447
2489
|
}) {
|
|
2448
2490
|
const actions = [];
|
|
2449
2491
|
/**
|
|
@@ -2492,6 +2534,26 @@ async function createValCodeActions({
|
|
|
2492
2534
|
});
|
|
2493
2535
|
continue;
|
|
2494
2536
|
}
|
|
2537
|
+
if (isWorkspaceEditFix(fix)) {
|
|
2538
|
+
if (offered.has(`${data.sourcePath}|${fix}`)) {
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
const action = createExtractEntryAction({
|
|
2542
|
+
document,
|
|
2543
|
+
sourcePath: data.sourcePath,
|
|
2544
|
+
content,
|
|
2545
|
+
valRoot,
|
|
2546
|
+
moduleFilePath,
|
|
2547
|
+
read,
|
|
2548
|
+
allowCreateFiles
|
|
2549
|
+
});
|
|
2550
|
+
if (!action) {
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
offered.add(`${data.sourcePath}|${fix}`);
|
|
2554
|
+
actions.push(action);
|
|
2555
|
+
continue;
|
|
2556
|
+
}
|
|
2495
2557
|
if (!isLocalFix(fix)) {
|
|
2496
2558
|
continue;
|
|
2497
2559
|
}
|
|
@@ -2534,6 +2596,131 @@ async function createValCodeActions({
|
|
|
2534
2596
|
}
|
|
2535
2597
|
return actions;
|
|
2536
2598
|
}
|
|
2599
|
+
|
|
2600
|
+
/**
|
|
2601
|
+
* The quick fix for `jsonValues:extract-entry`: move an entry that was written
|
|
2602
|
+
* inline in the `.val.ts` into its own `*.val.json`.
|
|
2603
|
+
*
|
|
2604
|
+
* Not a patch, and so not `computeFixEdit`. Two files change at once — a
|
|
2605
|
+
* `*.val.json` is created with the entry's content, and the `.val.ts` has the
|
|
2606
|
+
* inline value replaced by `c.json(() => import("./..."))` — and a `Patch` can
|
|
2607
|
+
* only edit one `.val.ts` and cannot create anything. That is why the fix is
|
|
2608
|
+
* `extractJsonValuesEntry` in the server rather than a `createFixPatch` branch,
|
|
2609
|
+
* and why the editor had no fix to offer for it at all until now: the patch
|
|
2610
|
+
* pipeline produced nothing and the action was dropped.
|
|
2611
|
+
*
|
|
2612
|
+
* The plan comes from the same `planJsonValuesEntryExtraction` that
|
|
2613
|
+
* `val validate --fix` runs, so the two cannot write different files; only the
|
|
2614
|
+
* last step differs. Here it is a `WorkspaceEdit`, so the change goes through
|
|
2615
|
+
* the editor's undo stack, and — crucially — is computed against the buffer the
|
|
2616
|
+
* user is looking at rather than what is on disk.
|
|
2617
|
+
*/
|
|
2618
|
+
function createExtractEntryAction({
|
|
2619
|
+
document,
|
|
2620
|
+
sourcePath,
|
|
2621
|
+
content,
|
|
2622
|
+
valRoot,
|
|
2623
|
+
moduleFilePath,
|
|
2624
|
+
read,
|
|
2625
|
+
allowCreateFiles
|
|
2626
|
+
}) {
|
|
2627
|
+
if (!moduleFilePath || !allowCreateFiles) {
|
|
2628
|
+
return undefined;
|
|
2629
|
+
}
|
|
2630
|
+
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
2631
|
+
const parts = core.Internal.splitModulePath(modulePath);
|
|
2632
|
+
// Root-only by contract, exactly as the server's fix handler asserts: the
|
|
2633
|
+
// entry is a direct child of the module's root record/router.
|
|
2634
|
+
if (parts.length !== 1) {
|
|
2635
|
+
return undefined;
|
|
2636
|
+
}
|
|
2637
|
+
const entryKey = parts[0];
|
|
2638
|
+
const entryContent = entryContentOf(content.source, entryKey);
|
|
2639
|
+
if (entryContent === undefined) {
|
|
2640
|
+
return undefined;
|
|
2641
|
+
}
|
|
2642
|
+
const planned = server.planJsonValuesEntryExtraction({
|
|
2643
|
+
moduleFilePath,
|
|
2644
|
+
entryKey,
|
|
2645
|
+
content: entryContent,
|
|
2646
|
+
valTsPath: uriToPath(document.uri),
|
|
2647
|
+
// The editor's text, not the file's: an edit computed against stale text
|
|
2648
|
+
// has ranges that land in the wrong place.
|
|
2649
|
+
valTsText: document.getText()
|
|
2650
|
+
});
|
|
2651
|
+
if (fp.result.isErr(planned)) {
|
|
2652
|
+
return undefined;
|
|
2653
|
+
}
|
|
2654
|
+
const absoluteJsonPath = path__default["default"].join(valRoot, planned.value.jsonPath);
|
|
2655
|
+
// Same refusal as the CLI's, widened by one case: an unsaved buffer counts as
|
|
2656
|
+
// occupied too. Creating over either would destroy content that a `--fix` run
|
|
2657
|
+
// refuses to touch.
|
|
2658
|
+
if (read?.(absoluteJsonPath) !== undefined || fs__default["default"].existsSync(absoluteJsonPath)) {
|
|
2659
|
+
return undefined;
|
|
2660
|
+
}
|
|
2661
|
+
const valTsEdit = minimalTextEdit(document.getText(), planned.value.valTsContent, document);
|
|
2662
|
+
if (!valTsEdit) {
|
|
2663
|
+
return undefined;
|
|
2664
|
+
}
|
|
2665
|
+
const jsonUri = pathToUri(absoluteJsonPath);
|
|
2666
|
+
return {
|
|
2667
|
+
title: FIX_TITLES["jsonValues:extract-entry"] ,
|
|
2668
|
+
kind: vscodeLanguageserver.CodeActionKind.QuickFix,
|
|
2669
|
+
edit: {
|
|
2670
|
+
// `documentChanges`, not `changes`: a plain edit map cannot create a file,
|
|
2671
|
+
// and an edit addressed to a file that does not exist is an error rather
|
|
2672
|
+
// than a creation.
|
|
2673
|
+
documentChanges: [{
|
|
2674
|
+
kind: "create",
|
|
2675
|
+
uri: jsonUri
|
|
2676
|
+
}, {
|
|
2677
|
+
textDocument: {
|
|
2678
|
+
uri: jsonUri,
|
|
2679
|
+
version: null
|
|
2680
|
+
},
|
|
2681
|
+
edits: [{
|
|
2682
|
+
range: {
|
|
2683
|
+
start: {
|
|
2684
|
+
line: 0,
|
|
2685
|
+
character: 0
|
|
2686
|
+
},
|
|
2687
|
+
end: {
|
|
2688
|
+
line: 0,
|
|
2689
|
+
character: 0
|
|
2690
|
+
}
|
|
2691
|
+
},
|
|
2692
|
+
newText: planned.value.jsonContent
|
|
2693
|
+
}]
|
|
2694
|
+
}, {
|
|
2695
|
+
textDocument: {
|
|
2696
|
+
uri: document.uri,
|
|
2697
|
+
version: null
|
|
2698
|
+
},
|
|
2699
|
+
edits: [valTsEdit]
|
|
2700
|
+
}]
|
|
2701
|
+
}
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
/**
|
|
2706
|
+
* One entry's value out of a module's source.
|
|
2707
|
+
*
|
|
2708
|
+
* `Object.entries` rather than an index: the source is a `Source`, which has no
|
|
2709
|
+
* index signature, and reading a dynamic key off it is the one thing this needs
|
|
2710
|
+
* to do. Mirrors what the server's fix handler reads, so the editor extracts the
|
|
2711
|
+
* same bytes `val validate --fix` would.
|
|
2712
|
+
*/
|
|
2713
|
+
function entryContentOf(source, entryKey) {
|
|
2714
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
2715
|
+
return undefined;
|
|
2716
|
+
}
|
|
2717
|
+
for (const [key, value] of Object.entries(source)) {
|
|
2718
|
+
if (key === entryKey) {
|
|
2719
|
+
return value;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return undefined;
|
|
2723
|
+
}
|
|
2537
2724
|
async function computeFixEdit({
|
|
2538
2725
|
document,
|
|
2539
2726
|
sourcePath,
|
|
@@ -3317,9 +3504,7 @@ function keysOfKeyOf(schema, snapshot) {
|
|
|
3317
3504
|
* was — worse than not offering the fix.
|
|
3318
3505
|
*/
|
|
3319
3506
|
function canRenameFiles(capabilities) {
|
|
3320
|
-
|
|
3321
|
-
const operations = workspace?.workspaceEdit?.resourceOperations;
|
|
3322
|
-
return Array.isArray(operations) && operations.includes("rename");
|
|
3507
|
+
return supportsResourceOperation(capabilities, "rename");
|
|
3323
3508
|
}
|
|
3324
3509
|
async function createGalleryMembershipActions({
|
|
3325
3510
|
document,
|
|
@@ -3792,6 +3977,7 @@ function createValLanguageServer(connection) {
|
|
|
3792
3977
|
const documents = new node.TextDocuments(vscodeLanguageserverTextdocument.TextDocument);
|
|
3793
3978
|
let canRegisterWatchers = false;
|
|
3794
3979
|
let canRenameFiles$1 = false;
|
|
3980
|
+
let canCreateFiles$1 = false;
|
|
3795
3981
|
|
|
3796
3982
|
/**
|
|
3797
3983
|
* The editor's view of a file, by absolute path, or `undefined` when the file
|
|
@@ -3955,6 +4141,10 @@ function createValLanguageServer(connection) {
|
|
|
3955
4141
|
// A RenameFile sent to a client that did not announce resourceOperations is
|
|
3956
4142
|
// silently dropped, which would rewrite the path and leave the file behind.
|
|
3957
4143
|
canRenameFiles$1 = canRenameFiles(params.capabilities);
|
|
4144
|
+
// Same reasoning for the extract-entry fix: it CREATES the `*.val.json` the
|
|
4145
|
+
// rewritten `.val.ts` imports, so a client that drops the create would be
|
|
4146
|
+
// left importing a file that is not there.
|
|
4147
|
+
canCreateFiles$1 = canCreateFiles(params.capabilities);
|
|
3958
4148
|
const clientCapabilities = params.capabilities.experimental?.val ?? {};
|
|
3959
4149
|
|
|
3960
4150
|
// Announce only what this version actually serves: a client hides UI for
|
|
@@ -4162,7 +4352,8 @@ function createValLanguageServer(connection) {
|
|
|
4162
4352
|
content: result.content,
|
|
4163
4353
|
valRoot: project.valRoot,
|
|
4164
4354
|
moduleFilePath,
|
|
4165
|
-
read: readOpenDocument
|
|
4355
|
+
read: readOpenDocument,
|
|
4356
|
+
allowCreateFiles: canCreateFiles$1
|
|
4166
4357
|
})));
|
|
4167
4358
|
return actions;
|
|
4168
4359
|
} catch (e) {
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import ts from 'typescript';
|
|
4
4
|
import { Internal, DEFAULT_VAL_REMOTE_HOST, DEFAULT_CONTENT_HOST } from '@valbuild/core';
|
|
5
|
-
import { fixHandlers, createService, analyzeValModule, createFixPatch, parsePersonalAccessTokenFile, getPersonalAccessTokenPath, startValLogin, awaitValLoginConfirmation, persistPersonalAccessToken, ValLoginError, findAndEvalValConfigFile, patchSourceFile, getSettings, uploadRemoteFile, findJsonEntryFilePath, rebaseContentOp, classifyJsonValuesOp, extractImageMetadata, extractFileMetadata } from '@valbuild/server';
|
|
5
|
+
import { fixHandlers, createService, analyzeValModule, createFixPatch, parsePersonalAccessTokenFile, getPersonalAccessTokenPath, startValLogin, awaitValLoginConfirmation, persistPersonalAccessToken, ValLoginError, findAndEvalValConfigFile, patchSourceFile, getSettings, uploadRemoteFile, findJsonEntryFilePath, rebaseContentOp, classifyJsonValuesOp, planJsonValuesEntryExtraction, extractImageMetadata, extractFileMetadata } from '@valbuild/server';
|
|
6
6
|
import { DiagnosticSeverity, ShowDocumentRequest, ApplyWorkspaceEditRequest, CodeActionKind, CodeAction, MarkupContent, CompletionItemKind, DidChangeWatchedFilesNotification } from 'vscode-languageserver';
|
|
7
7
|
import { TextDocuments, TextDocumentSyncKind, createConnection, ProposedFeatures } from 'vscode-languageserver/node';
|
|
8
8
|
import { TextDocument } from 'vscode-languageserver-textdocument';
|
|
@@ -2374,6 +2374,26 @@ function readFile(filePath) {
|
|
|
2374
2374
|
}
|
|
2375
2375
|
}
|
|
2376
2376
|
|
|
2377
|
+
/**
|
|
2378
|
+
* What the client announced it will do with a `WorkspaceEdit`.
|
|
2379
|
+
*
|
|
2380
|
+
* A resource operation — a file created, renamed or deleted as part of an edit —
|
|
2381
|
+
* is silently DROPPED by a client that did not announce it. The rest of the edit
|
|
2382
|
+
* still applies, so a fix that needs one and is offered anyway leaves the
|
|
2383
|
+
* project in a state that is worse than the one it set out to fix: a `.val.ts`
|
|
2384
|
+
* rewritten to import a file that was never created, a path rewritten to a file
|
|
2385
|
+
* that never moved. Hence: probe first, and do not offer the action at all.
|
|
2386
|
+
*
|
|
2387
|
+
* Read defensively. These come from `InitializeParams.capabilities`, which is
|
|
2388
|
+
* whatever the client sent — this is the one place that has to cope with the
|
|
2389
|
+
* field being absent or the wrong shape.
|
|
2390
|
+
*/
|
|
2391
|
+
function supportsResourceOperation(capabilities, operation) {
|
|
2392
|
+
const workspace = capabilities?.workspace;
|
|
2393
|
+
const operations = workspace?.workspaceEdit?.resourceOperations;
|
|
2394
|
+
return Array.isArray(operations) && operations.includes(operation);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2377
2397
|
/**
|
|
2378
2398
|
* Quick fixes, built by running Val's own fix machinery.
|
|
2379
2399
|
*
|
|
@@ -2397,6 +2417,18 @@ const LOCAL_FIXES = ["image:add-metadata", "image:check-metadata", "file:add-met
|
|
|
2397
2417
|
// stored metadata, dropping entries whose file has gone. Filesystem only.
|
|
2398
2418
|
"images:check-all-files", "files:check-all-files"];
|
|
2399
2419
|
|
|
2420
|
+
/**
|
|
2421
|
+
* Fixes that are local, but are NOT a patch against the document they are
|
|
2422
|
+
* reported on, so they never reach `computeFixEdit`.
|
|
2423
|
+
*
|
|
2424
|
+
* `jsonValues:extract-entry` creates a file and rewrites another — see
|
|
2425
|
+
* {@link createExtractEntryAction}. `createFixPatch` has no branch for it at all
|
|
2426
|
+
* (the fix lives in the server's own `extractJsonValuesEntry`), so routing it
|
|
2427
|
+
* through the patch pipeline would silently produce an empty patch and no
|
|
2428
|
+
* action, which is exactly what it did before this existed.
|
|
2429
|
+
*/
|
|
2430
|
+
const WORKSPACE_EDIT_FIXES = ["jsonValues:extract-entry"];
|
|
2431
|
+
|
|
2400
2432
|
/** Human-readable titles; falls back to the fix name for anything unknown. */
|
|
2401
2433
|
const FIX_TITLES = {
|
|
2402
2434
|
"image:add-metadata": "Val: add image metadata",
|
|
@@ -2404,11 +2436,20 @@ const FIX_TITLES = {
|
|
|
2404
2436
|
"file:add-metadata": "Val: add file metadata",
|
|
2405
2437
|
"file:check-metadata": "Val: update file metadata",
|
|
2406
2438
|
"images:check-all-files": "Val: update gallery image metadata",
|
|
2407
|
-
"files:check-all-files": "Val: update gallery file metadata"
|
|
2439
|
+
"files:check-all-files": "Val: update gallery file metadata",
|
|
2440
|
+
"jsonValues:extract-entry": "Val: move entry into its own .val.json"
|
|
2408
2441
|
};
|
|
2409
2442
|
function isLocalFix(fix) {
|
|
2410
2443
|
return LOCAL_FIXES.includes(fix);
|
|
2411
2444
|
}
|
|
2445
|
+
function isWorkspaceEditFix(fix) {
|
|
2446
|
+
return WORKSPACE_EDIT_FIXES.includes(fix);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
/** Whether the client will honour a file CREATE inside a `WorkspaceEdit`. */
|
|
2450
|
+
function canCreateFiles(capabilities) {
|
|
2451
|
+
return supportsResourceOperation(capabilities, "create");
|
|
2452
|
+
}
|
|
2412
2453
|
|
|
2413
2454
|
/**
|
|
2414
2455
|
* Build quick fixes for the diagnostics the client sent back.
|
|
@@ -2432,7 +2473,8 @@ async function createValCodeActions({
|
|
|
2432
2473
|
valRoot,
|
|
2433
2474
|
moduleFilePath,
|
|
2434
2475
|
read,
|
|
2435
|
-
remoteHost = process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST
|
|
2476
|
+
remoteHost = process.env.VAL_REMOTE_HOST || DEFAULT_VAL_REMOTE_HOST,
|
|
2477
|
+
allowCreateFiles = false
|
|
2436
2478
|
}) {
|
|
2437
2479
|
const actions = [];
|
|
2438
2480
|
/**
|
|
@@ -2481,6 +2523,26 @@ async function createValCodeActions({
|
|
|
2481
2523
|
});
|
|
2482
2524
|
continue;
|
|
2483
2525
|
}
|
|
2526
|
+
if (isWorkspaceEditFix(fix)) {
|
|
2527
|
+
if (offered.has(`${data.sourcePath}|${fix}`)) {
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2530
|
+
const action = createExtractEntryAction({
|
|
2531
|
+
document,
|
|
2532
|
+
sourcePath: data.sourcePath,
|
|
2533
|
+
content,
|
|
2534
|
+
valRoot,
|
|
2535
|
+
moduleFilePath,
|
|
2536
|
+
read,
|
|
2537
|
+
allowCreateFiles
|
|
2538
|
+
});
|
|
2539
|
+
if (!action) {
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
offered.add(`${data.sourcePath}|${fix}`);
|
|
2543
|
+
actions.push(action);
|
|
2544
|
+
continue;
|
|
2545
|
+
}
|
|
2484
2546
|
if (!isLocalFix(fix)) {
|
|
2485
2547
|
continue;
|
|
2486
2548
|
}
|
|
@@ -2523,6 +2585,131 @@ async function createValCodeActions({
|
|
|
2523
2585
|
}
|
|
2524
2586
|
return actions;
|
|
2525
2587
|
}
|
|
2588
|
+
|
|
2589
|
+
/**
|
|
2590
|
+
* The quick fix for `jsonValues:extract-entry`: move an entry that was written
|
|
2591
|
+
* inline in the `.val.ts` into its own `*.val.json`.
|
|
2592
|
+
*
|
|
2593
|
+
* Not a patch, and so not `computeFixEdit`. Two files change at once — a
|
|
2594
|
+
* `*.val.json` is created with the entry's content, and the `.val.ts` has the
|
|
2595
|
+
* inline value replaced by `c.json(() => import("./..."))` — and a `Patch` can
|
|
2596
|
+
* only edit one `.val.ts` and cannot create anything. That is why the fix is
|
|
2597
|
+
* `extractJsonValuesEntry` in the server rather than a `createFixPatch` branch,
|
|
2598
|
+
* and why the editor had no fix to offer for it at all until now: the patch
|
|
2599
|
+
* pipeline produced nothing and the action was dropped.
|
|
2600
|
+
*
|
|
2601
|
+
* The plan comes from the same `planJsonValuesEntryExtraction` that
|
|
2602
|
+
* `val validate --fix` runs, so the two cannot write different files; only the
|
|
2603
|
+
* last step differs. Here it is a `WorkspaceEdit`, so the change goes through
|
|
2604
|
+
* the editor's undo stack, and — crucially — is computed against the buffer the
|
|
2605
|
+
* user is looking at rather than what is on disk.
|
|
2606
|
+
*/
|
|
2607
|
+
function createExtractEntryAction({
|
|
2608
|
+
document,
|
|
2609
|
+
sourcePath,
|
|
2610
|
+
content,
|
|
2611
|
+
valRoot,
|
|
2612
|
+
moduleFilePath,
|
|
2613
|
+
read,
|
|
2614
|
+
allowCreateFiles
|
|
2615
|
+
}) {
|
|
2616
|
+
if (!moduleFilePath || !allowCreateFiles) {
|
|
2617
|
+
return undefined;
|
|
2618
|
+
}
|
|
2619
|
+
const [, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
2620
|
+
const parts = Internal.splitModulePath(modulePath);
|
|
2621
|
+
// Root-only by contract, exactly as the server's fix handler asserts: the
|
|
2622
|
+
// entry is a direct child of the module's root record/router.
|
|
2623
|
+
if (parts.length !== 1) {
|
|
2624
|
+
return undefined;
|
|
2625
|
+
}
|
|
2626
|
+
const entryKey = parts[0];
|
|
2627
|
+
const entryContent = entryContentOf(content.source, entryKey);
|
|
2628
|
+
if (entryContent === undefined) {
|
|
2629
|
+
return undefined;
|
|
2630
|
+
}
|
|
2631
|
+
const planned = planJsonValuesEntryExtraction({
|
|
2632
|
+
moduleFilePath,
|
|
2633
|
+
entryKey,
|
|
2634
|
+
content: entryContent,
|
|
2635
|
+
valTsPath: uriToPath(document.uri),
|
|
2636
|
+
// The editor's text, not the file's: an edit computed against stale text
|
|
2637
|
+
// has ranges that land in the wrong place.
|
|
2638
|
+
valTsText: document.getText()
|
|
2639
|
+
});
|
|
2640
|
+
if (result.isErr(planned)) {
|
|
2641
|
+
return undefined;
|
|
2642
|
+
}
|
|
2643
|
+
const absoluteJsonPath = path.join(valRoot, planned.value.jsonPath);
|
|
2644
|
+
// Same refusal as the CLI's, widened by one case: an unsaved buffer counts as
|
|
2645
|
+
// occupied too. Creating over either would destroy content that a `--fix` run
|
|
2646
|
+
// refuses to touch.
|
|
2647
|
+
if (read?.(absoluteJsonPath) !== undefined || fs.existsSync(absoluteJsonPath)) {
|
|
2648
|
+
return undefined;
|
|
2649
|
+
}
|
|
2650
|
+
const valTsEdit = minimalTextEdit(document.getText(), planned.value.valTsContent, document);
|
|
2651
|
+
if (!valTsEdit) {
|
|
2652
|
+
return undefined;
|
|
2653
|
+
}
|
|
2654
|
+
const jsonUri = pathToUri(absoluteJsonPath);
|
|
2655
|
+
return {
|
|
2656
|
+
title: FIX_TITLES["jsonValues:extract-entry"] ,
|
|
2657
|
+
kind: CodeActionKind.QuickFix,
|
|
2658
|
+
edit: {
|
|
2659
|
+
// `documentChanges`, not `changes`: a plain edit map cannot create a file,
|
|
2660
|
+
// and an edit addressed to a file that does not exist is an error rather
|
|
2661
|
+
// than a creation.
|
|
2662
|
+
documentChanges: [{
|
|
2663
|
+
kind: "create",
|
|
2664
|
+
uri: jsonUri
|
|
2665
|
+
}, {
|
|
2666
|
+
textDocument: {
|
|
2667
|
+
uri: jsonUri,
|
|
2668
|
+
version: null
|
|
2669
|
+
},
|
|
2670
|
+
edits: [{
|
|
2671
|
+
range: {
|
|
2672
|
+
start: {
|
|
2673
|
+
line: 0,
|
|
2674
|
+
character: 0
|
|
2675
|
+
},
|
|
2676
|
+
end: {
|
|
2677
|
+
line: 0,
|
|
2678
|
+
character: 0
|
|
2679
|
+
}
|
|
2680
|
+
},
|
|
2681
|
+
newText: planned.value.jsonContent
|
|
2682
|
+
}]
|
|
2683
|
+
}, {
|
|
2684
|
+
textDocument: {
|
|
2685
|
+
uri: document.uri,
|
|
2686
|
+
version: null
|
|
2687
|
+
},
|
|
2688
|
+
edits: [valTsEdit]
|
|
2689
|
+
}]
|
|
2690
|
+
}
|
|
2691
|
+
};
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
/**
|
|
2695
|
+
* One entry's value out of a module's source.
|
|
2696
|
+
*
|
|
2697
|
+
* `Object.entries` rather than an index: the source is a `Source`, which has no
|
|
2698
|
+
* index signature, and reading a dynamic key off it is the one thing this needs
|
|
2699
|
+
* to do. Mirrors what the server's fix handler reads, so the editor extracts the
|
|
2700
|
+
* same bytes `val validate --fix` would.
|
|
2701
|
+
*/
|
|
2702
|
+
function entryContentOf(source, entryKey) {
|
|
2703
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) {
|
|
2704
|
+
return undefined;
|
|
2705
|
+
}
|
|
2706
|
+
for (const [key, value] of Object.entries(source)) {
|
|
2707
|
+
if (key === entryKey) {
|
|
2708
|
+
return value;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
return undefined;
|
|
2712
|
+
}
|
|
2526
2713
|
async function computeFixEdit({
|
|
2527
2714
|
document,
|
|
2528
2715
|
sourcePath,
|
|
@@ -3306,9 +3493,7 @@ function keysOfKeyOf(schema, snapshot) {
|
|
|
3306
3493
|
* was — worse than not offering the fix.
|
|
3307
3494
|
*/
|
|
3308
3495
|
function canRenameFiles(capabilities) {
|
|
3309
|
-
|
|
3310
|
-
const operations = workspace?.workspaceEdit?.resourceOperations;
|
|
3311
|
-
return Array.isArray(operations) && operations.includes("rename");
|
|
3496
|
+
return supportsResourceOperation(capabilities, "rename");
|
|
3312
3497
|
}
|
|
3313
3498
|
async function createGalleryMembershipActions({
|
|
3314
3499
|
document,
|
|
@@ -3781,6 +3966,7 @@ function createValLanguageServer(connection) {
|
|
|
3781
3966
|
const documents = new TextDocuments(TextDocument);
|
|
3782
3967
|
let canRegisterWatchers = false;
|
|
3783
3968
|
let canRenameFiles$1 = false;
|
|
3969
|
+
let canCreateFiles$1 = false;
|
|
3784
3970
|
|
|
3785
3971
|
/**
|
|
3786
3972
|
* The editor's view of a file, by absolute path, or `undefined` when the file
|
|
@@ -3944,6 +4130,10 @@ function createValLanguageServer(connection) {
|
|
|
3944
4130
|
// A RenameFile sent to a client that did not announce resourceOperations is
|
|
3945
4131
|
// silently dropped, which would rewrite the path and leave the file behind.
|
|
3946
4132
|
canRenameFiles$1 = canRenameFiles(params.capabilities);
|
|
4133
|
+
// Same reasoning for the extract-entry fix: it CREATES the `*.val.json` the
|
|
4134
|
+
// rewritten `.val.ts` imports, so a client that drops the create would be
|
|
4135
|
+
// left importing a file that is not there.
|
|
4136
|
+
canCreateFiles$1 = canCreateFiles(params.capabilities);
|
|
3947
4137
|
const clientCapabilities = params.capabilities.experimental?.val ?? {};
|
|
3948
4138
|
|
|
3949
4139
|
// Announce only what this version actually serves: a client hides UI for
|
|
@@ -4151,7 +4341,8 @@ function createValLanguageServer(connection) {
|
|
|
4151
4341
|
content: result.content,
|
|
4152
4342
|
valRoot: project.valRoot,
|
|
4153
4343
|
moduleFilePath,
|
|
4154
|
-
read: readOpenDocument
|
|
4344
|
+
read: readOpenDocument,
|
|
4345
|
+
allowCreateFiles: canCreateFiles$1
|
|
4155
4346
|
})));
|
|
4156
4347
|
return actions;
|
|
4157
4348
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"lsp",
|
|
12
12
|
"language-server"
|
|
13
13
|
],
|
|
14
|
-
"version": "0.
|
|
14
|
+
"version": "0.124.0",
|
|
15
15
|
"bin": {
|
|
16
16
|
"val-language-server": "./bin.js"
|
|
17
17
|
},
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
30
|
"vscode-languageserver": "^10.1.0",
|
|
31
31
|
"vscode-languageserver-textdocument": "^1.0.14",
|
|
32
|
-
"@valbuild/
|
|
33
|
-
"@valbuild/
|
|
34
|
-
"@valbuild/
|
|
32
|
+
"@valbuild/core": "0.124.0",
|
|
33
|
+
"@valbuild/server": "0.124.0",
|
|
34
|
+
"@valbuild/shared": "0.124.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/jest": "^30.0.0",
|