@valbuild/language-server 0.140.2 → 0.141.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/CHANGELOG.md +77 -0
- package/dist/declarations/src/commands.d.ts +1 -1
- package/dist/declarations/src/completionContext.d.ts +1 -1
- package/dist/declarations/src/completions.d.ts +1 -1
- package/dist/declarations/src/diagnostics.d.ts +5 -3
- package/dist/declarations/src/publicValFiles.d.ts +11 -4
- package/dist/valbuild-language-server.cjs.dev.js +238 -138
- package/dist/valbuild-language-server.cjs.prod.js +238 -138
- package/dist/valbuild-language-server.esm.js +238 -138
- package/package.json +4 -4
|
@@ -6,12 +6,12 @@ var fs = require('fs');
|
|
|
6
6
|
var path = require('path');
|
|
7
7
|
var ts = require('typescript');
|
|
8
8
|
var core = require('@valbuild/core');
|
|
9
|
-
var server = require('@valbuild/server');
|
|
10
9
|
var vscodeLanguageserver = require('vscode-languageserver');
|
|
11
10
|
var node = require('vscode-languageserver/node');
|
|
12
11
|
var vscodeLanguageserverTextdocument = require('vscode-languageserver-textdocument');
|
|
13
12
|
var crypto = require('crypto');
|
|
14
13
|
var node_module = require('node:module');
|
|
14
|
+
var server = require('@valbuild/server');
|
|
15
15
|
var internal = require('@valbuild/shared/internal');
|
|
16
16
|
var fp = require('@valbuild/core/fp');
|
|
17
17
|
var patch = require('@valbuild/core/patch');
|
|
@@ -183,7 +183,7 @@ var packageJson = {
|
|
|
183
183
|
"lsp",
|
|
184
184
|
"language-server"
|
|
185
185
|
],
|
|
186
|
-
version: "0.
|
|
186
|
+
version: "0.141.1",
|
|
187
187
|
bin: {
|
|
188
188
|
"val-language-server": "./bin.js"
|
|
189
189
|
},
|
|
@@ -1414,7 +1414,11 @@ const FALLBACK_RANGE = {
|
|
|
1414
1414
|
* upload/download fixes stay: `*:upload-remote` is reported on a local path
|
|
1415
1415
|
* and a missing file really is its problem.
|
|
1416
1416
|
*/
|
|
1417
|
-
const FILE_FIXES = ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote"
|
|
1417
|
+
const FILE_FIXES = ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote",
|
|
1418
|
+
// Local only: core never asks a remote video for its metadata.
|
|
1419
|
+
"video:add-metadata",
|
|
1420
|
+
// The same, for a video set's entry, whose file is its KEY.
|
|
1421
|
+
"videos:add-metadata"];
|
|
1418
1422
|
function build(range, message, data) {
|
|
1419
1423
|
return {
|
|
1420
1424
|
range,
|
|
@@ -1493,7 +1497,8 @@ function createValDiagnostics({
|
|
|
1493
1497
|
const missing = valRoot && fixes?.some(fix => FILE_FIXES.includes(fix)) ? missingFileRef({
|
|
1494
1498
|
sourcePath,
|
|
1495
1499
|
content,
|
|
1496
|
-
valRoot
|
|
1500
|
+
valRoot,
|
|
1501
|
+
fixes
|
|
1497
1502
|
}) : undefined;
|
|
1498
1503
|
if (missing) {
|
|
1499
1504
|
diagnostics.push(build(rangeOf(sourcePath, modulePathMap, "path"), `File ${missing} does not exist`, {
|
|
@@ -1578,7 +1583,8 @@ function createValDiagnostics({
|
|
|
1578
1583
|
function missingFileRef({
|
|
1579
1584
|
sourcePath,
|
|
1580
1585
|
content,
|
|
1581
|
-
valRoot
|
|
1586
|
+
valRoot,
|
|
1587
|
+
fixes
|
|
1582
1588
|
}) {
|
|
1583
1589
|
if (!content.source || !content.schema) {
|
|
1584
1590
|
return undefined;
|
|
@@ -1586,7 +1592,9 @@ function missingFileRef({
|
|
|
1586
1592
|
try {
|
|
1587
1593
|
const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
|
|
1588
1594
|
const resolved = core.Internal.resolvePath(modulePath, content.source, content.schema);
|
|
1589
|
-
const ref =
|
|
1595
|
+
const ref = fixes.includes("videos:add-metadata") ?
|
|
1596
|
+
// A video set entry is keyed by its file; the value has no `path`.
|
|
1597
|
+
core.Internal.splitModulePath(modulePath).pop() : resolved.source?.path;
|
|
1590
1598
|
if (typeof ref !== "string") {
|
|
1591
1599
|
return undefined;
|
|
1592
1600
|
}
|
|
@@ -1721,7 +1729,7 @@ function longestResolvedPrefixRange(modulePath, modulePathMap) {
|
|
|
1721
1729
|
* Gallery checks core emits unconditionally.
|
|
1722
1730
|
*
|
|
1723
1731
|
* `RecordSchema.validate` attaches these to every `s.imageset()` / `s.fileset()`
|
|
1724
|
-
* module whether or not anything is actually wrong (see
|
|
1732
|
+
* / `s.videoset()` module whether or not anything is actually wrong (see
|
|
1725
1733
|
* `packages/core/src/schema/record.ts`): they are placeholders asking someone to
|
|
1726
1734
|
* go and look. `val validate` looks by running the matching fix handler, which
|
|
1727
1735
|
* reports success when the directory is unique and every file is accounted for.
|
|
@@ -1730,7 +1738,7 @@ function longestResolvedPrefixRange(modulePath, modulePathMap) {
|
|
|
1730
1738
|
* on every gallery module in the project — which is worse than useless, because
|
|
1731
1739
|
* a warning that is always there is one nobody reads.
|
|
1732
1740
|
*/
|
|
1733
|
-
const GALLERY_CHECK_FIXES = ["images:check-unique-folder", "files:check-unique-folder", "images:check-all-files", "files:check-all-files"];
|
|
1741
|
+
const GALLERY_CHECK_FIXES = ["images:check-unique-folder", "files:check-unique-folder", "videos:check-unique-folder", "images:check-all-files", "files:check-all-files", "videos:check-all-files"];
|
|
1734
1742
|
function isGalleryCheckFix(fix) {
|
|
1735
1743
|
return GALLERY_CHECK_FIXES.includes(fix);
|
|
1736
1744
|
}
|
|
@@ -1830,7 +1838,9 @@ function dropDeferredPlaceholders(validation) {
|
|
|
1830
1838
|
* A gallery-backed field pointing at something the gallery does not have.
|
|
1831
1839
|
*
|
|
1832
1840
|
* `ImageSchema.validate` reports this (`packages/core/src/schema/image.ts`:
|
|
1833
|
-
* "The gallery does not have an image at '…'")
|
|
1841
|
+
* "The gallery does not have an image at '…'"), and `VideoSchema.validate` the
|
|
1842
|
+
* same for a field picked from an `s.videoset()` ("The set does not have a
|
|
1843
|
+
* video at '…'"), but neither attaches a `ValidationFix`,
|
|
1834
1844
|
* because the remedy is not a change to this module: either the gallery gains an
|
|
1835
1845
|
* entry, or the file moves into the gallery's directory. Both are edits to
|
|
1836
1846
|
* somewhere else, which is not what a `ValidationFix` describes.
|
|
@@ -1856,7 +1866,7 @@ function galleryMembershipAt({
|
|
|
1856
1866
|
return undefined;
|
|
1857
1867
|
}
|
|
1858
1868
|
const schema = resolved.schema;
|
|
1859
|
-
if (!schema || typeof schema !== "object" || !("type" in schema) || schema.type !== "image" && schema.type !== "file") {
|
|
1869
|
+
if (!schema || typeof schema !== "object" || !("type" in schema) || schema.type !== "image" && schema.type !== "file" && schema.type !== "video") {
|
|
1860
1870
|
return undefined;
|
|
1861
1871
|
}
|
|
1862
1872
|
const referencedModule = "referencedModule" in schema && typeof schema.referencedModule === "string" ? schema.referencedModule : undefined;
|
|
@@ -1923,6 +1933,100 @@ function minimalTextEdit(before, after, document) {
|
|
|
1923
1933
|
};
|
|
1924
1934
|
}
|
|
1925
1935
|
|
|
1936
|
+
/**
|
|
1937
|
+
* Conversions between LSP document URIs and the paths Val uses.
|
|
1938
|
+
*
|
|
1939
|
+
* Deliberately minimal rather than pulling in `vscode-uri`: the server only ever
|
|
1940
|
+
* deals with local `file:` URIs, and keeping this small makes the assumptions
|
|
1941
|
+
* visible.
|
|
1942
|
+
*/
|
|
1943
|
+
|
|
1944
|
+
/** Matches the Val module files the server validates. */
|
|
1945
|
+
const VAL_MODULE_RE = /\.val\.(ts|js|tsx|jsx)$/;
|
|
1946
|
+
|
|
1947
|
+
/** `file:` with an optional authority, capturing authority and path apart. */
|
|
1948
|
+
const FILE_URI_RE = /^file:\/\/([^/?#]*)([^?#]*)/i;
|
|
1949
|
+
|
|
1950
|
+
/** A `/c:/...` prefix, i.e. a Windows drive letter as it appears in a URI. */
|
|
1951
|
+
const URI_DRIVE_LETTER_RE = /^\/([a-zA-Z]:)(\/|$)/;
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* Matches the `*.val.json` files that hold `.jsonValues()` entry content. Not a
|
|
1955
|
+
* Val module: nothing validates one on its own, but editing it changes what a
|
|
1956
|
+
* module validates to, so the server has to notice.
|
|
1957
|
+
*/
|
|
1958
|
+
const VAL_JSON_ENTRY_RE = /\.val\.json$/;
|
|
1959
|
+
function isValModuleUri(uri) {
|
|
1960
|
+
return VAL_MODULE_RE.test(uri);
|
|
1961
|
+
}
|
|
1962
|
+
function isValJsonEntryUri(uri) {
|
|
1963
|
+
return VAL_JSON_ENTRY_RE.test(uri);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
/**
|
|
1967
|
+
* `decodeURIComponent` throws on malformed escapes (`%zz`). A client that sends
|
|
1968
|
+
* one is broken, but that should not take the server down: fall back to the
|
|
1969
|
+
* undecoded text so the path is at worst not found.
|
|
1970
|
+
*/
|
|
1971
|
+
function decodeSafely(value) {
|
|
1972
|
+
try {
|
|
1973
|
+
return decodeURIComponent(value);
|
|
1974
|
+
} catch {
|
|
1975
|
+
return value;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* `file:///a/b.val.ts` -> `/a/b.val.ts`
|
|
1981
|
+
*
|
|
1982
|
+
* Percent escapes are decoded, Windows drive letters lose the leading slash
|
|
1983
|
+
* (`file:///c%3A/a` -> `c:/a`), and a URI with an authority is read as a UNC
|
|
1984
|
+
* path (`file://host/share/a` -> `//host/share/a`). Anything that is not a
|
|
1985
|
+
* `file:` URI is passed through unchanged, since callers also hand us plain
|
|
1986
|
+
* paths.
|
|
1987
|
+
*/
|
|
1988
|
+
function uriToPath(uri) {
|
|
1989
|
+
const match = FILE_URI_RE.exec(uri);
|
|
1990
|
+
if (!match) {
|
|
1991
|
+
return uri;
|
|
1992
|
+
}
|
|
1993
|
+
const authority = decodeSafely(match[1]);
|
|
1994
|
+
const fsPath = decodeSafely(match[2] || "/");
|
|
1995
|
+
if (authority) {
|
|
1996
|
+
return `//${authority}${fsPath}`;
|
|
1997
|
+
}
|
|
1998
|
+
return fsPath.replace(URI_DRIVE_LETTER_RE, "$1$2");
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* `/a/b.val.ts` -> `file:///a/b.val.ts`
|
|
2003
|
+
*
|
|
2004
|
+
* The escaping matches what VS Code produces (drive-letter colons included), so
|
|
2005
|
+
* that a URI built here can be looked up in the open-document map keyed by the
|
|
2006
|
+
* URIs the client sent.
|
|
2007
|
+
*/
|
|
2008
|
+
function pathToUri(fsPath) {
|
|
2009
|
+
const normalized = fsPath.split(path__default["default"].sep).join("/");
|
|
2010
|
+
const rooted = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
2011
|
+
return `file://${rooted.split("/").map(segment => encodeURIComponent(segment)).join("/")}`;
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Convert a document URI into the `ModuleFilePath` Val addresses it by: a
|
|
2016
|
+
* POSIX-style path relative to the Val root, with a leading slash.
|
|
2017
|
+
*
|
|
2018
|
+
* Returns `undefined` when the file lies outside the Val root — one server
|
|
2019
|
+
* serves exactly one root, so another root's files are not its business.
|
|
2020
|
+
*/
|
|
2021
|
+
function toModuleFilePath(valRoot, uri) {
|
|
2022
|
+
const fsPath = uriToPath(uri);
|
|
2023
|
+
const relative = path__default["default"].relative(valRoot, fsPath);
|
|
2024
|
+
if (!relative || relative.startsWith("..") || path__default["default"].isAbsolute(relative)) {
|
|
2025
|
+
return undefined;
|
|
2026
|
+
}
|
|
2027
|
+
return `/${relative.split(path__default["default"].sep).join("/")}`;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1926
2030
|
/**
|
|
1927
2031
|
* `workspace/executeCommand` handlers.
|
|
1928
2032
|
*
|
|
@@ -1959,7 +2063,10 @@ const REMOTE_FIX_COMMANDS = {
|
|
|
1959
2063
|
"images:upload-remote": VAL_UPLOAD_REMOTE_COMMAND,
|
|
1960
2064
|
"files:upload-remote": VAL_UPLOAD_REMOTE_COMMAND,
|
|
1961
2065
|
"image:download-remote": VAL_DOWNLOAD_REMOTE_COMMAND,
|
|
1962
|
-
"file:download-remote": VAL_DOWNLOAD_REMOTE_COMMAND
|
|
2066
|
+
"file:download-remote": VAL_DOWNLOAD_REMOTE_COMMAND,
|
|
2067
|
+
"video:upload-remote": VAL_UPLOAD_REMOTE_COMMAND,
|
|
2068
|
+
"video:download-remote": VAL_DOWNLOAD_REMOTE_COMMAND,
|
|
2069
|
+
"videos:upload-remote": VAL_UPLOAD_REMOTE_COMMAND
|
|
1963
2070
|
};
|
|
1964
2071
|
const REMOTE_FIX_TITLES = {
|
|
1965
2072
|
"image:upload-remote": "Val: upload this image to Val Remote",
|
|
@@ -1967,7 +2074,10 @@ const REMOTE_FIX_TITLES = {
|
|
|
1967
2074
|
"images:upload-remote": "Val: upload this gallery's images to Val Remote",
|
|
1968
2075
|
"files:upload-remote": "Val: upload this gallery's files to Val Remote",
|
|
1969
2076
|
"image:download-remote": "Val: download this image into the project",
|
|
1970
|
-
"file:download-remote": "Val: download this file into the project"
|
|
2077
|
+
"file:download-remote": "Val: download this file into the project",
|
|
2078
|
+
"video:upload-remote": "Val: upload this video (and its poster and captions) to Val Remote",
|
|
2079
|
+
"video:download-remote": "Val: download this video (and its poster and captions) into the project",
|
|
2080
|
+
"videos:upload-remote": "Val: upload this video to Val Remote (and point the fields using it there)"
|
|
1971
2081
|
};
|
|
1972
2082
|
|
|
1973
2083
|
/** Arguments a remote-fix command is invoked with. */
|
|
@@ -2184,16 +2294,67 @@ function createValCommands(deps) {
|
|
|
2184
2294
|
if (!edit) {
|
|
2185
2295
|
return;
|
|
2186
2296
|
}
|
|
2297
|
+
const changes = {
|
|
2298
|
+
[args.uri]: [edit]
|
|
2299
|
+
};
|
|
2300
|
+
// A fix that renames what other modules name (a video set's key) rewrites
|
|
2301
|
+
// them in the same edit, so one undo takes back all of it.
|
|
2302
|
+
const unwritten = [];
|
|
2303
|
+
for (const other of outcome.otherModulePatches ?? []) {
|
|
2304
|
+
const otherEdit = editForModulePatch(project.valRoot, other);
|
|
2305
|
+
if (otherEdit) {
|
|
2306
|
+
(changes[otherEdit.uri] ??= []).push(otherEdit.edit);
|
|
2307
|
+
} else {
|
|
2308
|
+
unwritten.push(other.moduleFilePath);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2187
2311
|
// Applied through the client so it lands in the editor's undo history,
|
|
2188
2312
|
// rather than written to disk under the user's cursor.
|
|
2189
|
-
await connection.sendRequest(vscodeLanguageserver.ApplyWorkspaceEditRequest.type, {
|
|
2313
|
+
const applied = await connection.sendRequest(vscodeLanguageserver.ApplyWorkspaceEditRequest.type, {
|
|
2190
2314
|
label: REMOTE_FIX_TITLES[args.fix] ?? `Val: ${args.fix}`,
|
|
2191
2315
|
edit: {
|
|
2192
|
-
changes
|
|
2193
|
-
[args.uri]: [edit]
|
|
2194
|
-
}
|
|
2316
|
+
changes
|
|
2195
2317
|
}
|
|
2196
2318
|
});
|
|
2319
|
+
// The client may refuse the edit — a document changed under it, say. The
|
|
2320
|
+
// files have moved by then, but nothing names them yet, so the fix did
|
|
2321
|
+
// NOT happen and must not read as though it had. Running it again is
|
|
2322
|
+
// safe: refs are content-addressed, so the same bytes go to the same refs.
|
|
2323
|
+
if (!applied.applied) {
|
|
2324
|
+
connection.window.showErrorMessage(`Val: ${args.fix} moved the files, but the editor did not apply the change${applied.failureReason ? ` (${applied.failureReason})` : ""}. Nothing was rewritten: run the fix again.`);
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
if (unwritten.length > 0) {
|
|
2328
|
+
connection.window.showWarningMessage(`Val: ${args.fix} could not rewrite ${unwritten.join(", ")}. Run "val validate" to see what still points at the old path.`);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
/**
|
|
2333
|
+
* The edit for a patch to another module than the one the command ran on,
|
|
2334
|
+
* against the editor's buffer when the file is open and the disk otherwise.
|
|
2335
|
+
*/
|
|
2336
|
+
function editForModulePatch(valRoot, other) {
|
|
2337
|
+
const uri = pathToUri(path__default["default"].join(valRoot, other.moduleFilePath));
|
|
2338
|
+
let otherDocument = deps.getDocument(uri);
|
|
2339
|
+
if (!otherDocument) {
|
|
2340
|
+
let text;
|
|
2341
|
+
try {
|
|
2342
|
+
text = fs__default["default"].readFileSync(path__default["default"].join(valRoot, other.moduleFilePath), "utf8");
|
|
2343
|
+
} catch {
|
|
2344
|
+
return undefined;
|
|
2345
|
+
}
|
|
2346
|
+
otherDocument = vscodeLanguageserverTextdocument.TextDocument.create(uri, "typescript", 0, text);
|
|
2347
|
+
}
|
|
2348
|
+
const otherBefore = otherDocument.getText();
|
|
2349
|
+
const otherPatched = server.patchSourceFile(otherBefore, other.patch);
|
|
2350
|
+
if (fp.result.isErr(otherPatched)) {
|
|
2351
|
+
return undefined;
|
|
2352
|
+
}
|
|
2353
|
+
const otherEdit = minimalTextEdit(otherBefore, otherPatched.value.text, otherDocument);
|
|
2354
|
+
return otherEdit && {
|
|
2355
|
+
uri,
|
|
2356
|
+
edit: otherEdit
|
|
2357
|
+
};
|
|
2197
2358
|
}
|
|
2198
2359
|
return {
|
|
2199
2360
|
async execute(command, args) {
|
|
@@ -2376,100 +2537,6 @@ function valModulesEntryText({
|
|
|
2376
2537
|
return hasElements ? `,\n${indentation}${entry}` : `\n${indentation}${entry}\n${indentation.slice(2)}`;
|
|
2377
2538
|
}
|
|
2378
2539
|
|
|
2379
|
-
/**
|
|
2380
|
-
* Conversions between LSP document URIs and the paths Val uses.
|
|
2381
|
-
*
|
|
2382
|
-
* Deliberately minimal rather than pulling in `vscode-uri`: the server only ever
|
|
2383
|
-
* deals with local `file:` URIs, and keeping this small makes the assumptions
|
|
2384
|
-
* visible.
|
|
2385
|
-
*/
|
|
2386
|
-
|
|
2387
|
-
/** Matches the Val module files the server validates. */
|
|
2388
|
-
const VAL_MODULE_RE = /\.val\.(ts|js|tsx|jsx)$/;
|
|
2389
|
-
|
|
2390
|
-
/** `file:` with an optional authority, capturing authority and path apart. */
|
|
2391
|
-
const FILE_URI_RE = /^file:\/\/([^/?#]*)([^?#]*)/i;
|
|
2392
|
-
|
|
2393
|
-
/** A `/c:/...` prefix, i.e. a Windows drive letter as it appears in a URI. */
|
|
2394
|
-
const URI_DRIVE_LETTER_RE = /^\/([a-zA-Z]:)(\/|$)/;
|
|
2395
|
-
|
|
2396
|
-
/**
|
|
2397
|
-
* Matches the `*.val.json` files that hold `.jsonValues()` entry content. Not a
|
|
2398
|
-
* Val module: nothing validates one on its own, but editing it changes what a
|
|
2399
|
-
* module validates to, so the server has to notice.
|
|
2400
|
-
*/
|
|
2401
|
-
const VAL_JSON_ENTRY_RE = /\.val\.json$/;
|
|
2402
|
-
function isValModuleUri(uri) {
|
|
2403
|
-
return VAL_MODULE_RE.test(uri);
|
|
2404
|
-
}
|
|
2405
|
-
function isValJsonEntryUri(uri) {
|
|
2406
|
-
return VAL_JSON_ENTRY_RE.test(uri);
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
/**
|
|
2410
|
-
* `decodeURIComponent` throws on malformed escapes (`%zz`). A client that sends
|
|
2411
|
-
* one is broken, but that should not take the server down: fall back to the
|
|
2412
|
-
* undecoded text so the path is at worst not found.
|
|
2413
|
-
*/
|
|
2414
|
-
function decodeSafely(value) {
|
|
2415
|
-
try {
|
|
2416
|
-
return decodeURIComponent(value);
|
|
2417
|
-
} catch {
|
|
2418
|
-
return value;
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
|
-
/**
|
|
2423
|
-
* `file:///a/b.val.ts` -> `/a/b.val.ts`
|
|
2424
|
-
*
|
|
2425
|
-
* Percent escapes are decoded, Windows drive letters lose the leading slash
|
|
2426
|
-
* (`file:///c%3A/a` -> `c:/a`), and a URI with an authority is read as a UNC
|
|
2427
|
-
* path (`file://host/share/a` -> `//host/share/a`). Anything that is not a
|
|
2428
|
-
* `file:` URI is passed through unchanged, since callers also hand us plain
|
|
2429
|
-
* paths.
|
|
2430
|
-
*/
|
|
2431
|
-
function uriToPath(uri) {
|
|
2432
|
-
const match = FILE_URI_RE.exec(uri);
|
|
2433
|
-
if (!match) {
|
|
2434
|
-
return uri;
|
|
2435
|
-
}
|
|
2436
|
-
const authority = decodeSafely(match[1]);
|
|
2437
|
-
const fsPath = decodeSafely(match[2] || "/");
|
|
2438
|
-
if (authority) {
|
|
2439
|
-
return `//${authority}${fsPath}`;
|
|
2440
|
-
}
|
|
2441
|
-
return fsPath.replace(URI_DRIVE_LETTER_RE, "$1$2");
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
/**
|
|
2445
|
-
* `/a/b.val.ts` -> `file:///a/b.val.ts`
|
|
2446
|
-
*
|
|
2447
|
-
* The escaping matches what VS Code produces (drive-letter colons included), so
|
|
2448
|
-
* that a URI built here can be looked up in the open-document map keyed by the
|
|
2449
|
-
* URIs the client sent.
|
|
2450
|
-
*/
|
|
2451
|
-
function pathToUri(fsPath) {
|
|
2452
|
-
const normalized = fsPath.split(path__default["default"].sep).join("/");
|
|
2453
|
-
const rooted = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
|
2454
|
-
return `file://${rooted.split("/").map(segment => encodeURIComponent(segment)).join("/")}`;
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
/**
|
|
2458
|
-
* Convert a document URI into the `ModuleFilePath` Val addresses it by: a
|
|
2459
|
-
* POSIX-style path relative to the Val root, with a leading slash.
|
|
2460
|
-
*
|
|
2461
|
-
* Returns `undefined` when the file lies outside the Val root — one server
|
|
2462
|
-
* serves exactly one root, so another root's files are not its business.
|
|
2463
|
-
*/
|
|
2464
|
-
function toModuleFilePath(valRoot, uri) {
|
|
2465
|
-
const fsPath = uriToPath(uri);
|
|
2466
|
-
const relative = path__default["default"].relative(valRoot, fsPath);
|
|
2467
|
-
if (!relative || relative.startsWith("..") || path__default["default"].isAbsolute(relative)) {
|
|
2468
|
-
return undefined;
|
|
2469
|
-
}
|
|
2470
|
-
return `/${relative.split(path__default["default"].sep).join("/")}`;
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
2540
|
const jsonOps = new patch.JSONOps();
|
|
2474
2541
|
|
|
2475
2542
|
/**
|
|
@@ -2647,9 +2714,19 @@ function supportsResourceOperation(capabilities, operation) {
|
|
|
2647
2714
|
* silently required auth would just fail.
|
|
2648
2715
|
*/
|
|
2649
2716
|
const LOCAL_FIXES = ["image:add-metadata", "image:check-metadata", "file:add-metadata", "file:check-metadata",
|
|
2717
|
+
// A local video's mimeType, width, height and duration, read from the file
|
|
2718
|
+
// (or, for an HLS stream, from its playlists) on disk.
|
|
2719
|
+
"video:add-metadata",
|
|
2650
2720
|
// Gallery metadata: createFixPatch reads each entry's file and corrects the
|
|
2651
2721
|
// stored metadata, dropping entries whose file has gone. Filesystem only.
|
|
2652
2722
|
"images:check-all-files", "files:check-all-files",
|
|
2723
|
+
// A video set's entries against its directory: entries whose file has gone
|
|
2724
|
+
// are dropped, and videos in the directory the set does not list are added
|
|
2725
|
+
// with what is read from their bytes. Filesystem only.
|
|
2726
|
+
"videos:check-all-files",
|
|
2727
|
+
// A video set entry's mimeType, width, height and duration, read from the
|
|
2728
|
+
// file at its KEY. The set's twin of `video:add-metadata`.
|
|
2729
|
+
"videos:add-metadata",
|
|
2653
2730
|
// A view pointing at the wrong module: the schema names the right one, so the
|
|
2654
2731
|
// fix is a value the patch already knows. Nothing read, nothing fetched.
|
|
2655
2732
|
"view:check-module"];
|
|
@@ -2672,8 +2749,11 @@ const FIX_TITLES = {
|
|
|
2672
2749
|
"image:check-metadata": "Val: update image metadata",
|
|
2673
2750
|
"file:add-metadata": "Val: add file metadata",
|
|
2674
2751
|
"file:check-metadata": "Val: update file metadata",
|
|
2752
|
+
"video:add-metadata": "Val: add video metadata",
|
|
2675
2753
|
"images:check-all-files": "Val: update gallery image metadata",
|
|
2676
2754
|
"files:check-all-files": "Val: update gallery file metadata",
|
|
2755
|
+
"videos:check-all-files": "Val: sync the video set with its directory",
|
|
2756
|
+
"videos:add-metadata": "Val: add video metadata",
|
|
2677
2757
|
"jsonValues:extract-entry": "Val: move entry into its own .val.json",
|
|
2678
2758
|
"view:check-module": "Val: point this view at the module its schema names"
|
|
2679
2759
|
};
|
|
@@ -3241,7 +3321,9 @@ function getValCompletionContext(sourceFile, offset) {
|
|
|
3241
3321
|
}
|
|
3242
3322
|
|
|
3243
3323
|
/** The properties Val computes from a file's bytes. */
|
|
3244
|
-
const MEDIA_METADATA_KEYS = ["width", "height", "mimeType"
|
|
3324
|
+
const MEDIA_METADATA_KEYS = ["width", "height", "mimeType",
|
|
3325
|
+
// A video's: never read for an image or a file, so never written for one.
|
|
3326
|
+
"duration"];
|
|
3245
3327
|
/**
|
|
3246
3328
|
* Re-find the media object literal whose `path` value starts at
|
|
3247
3329
|
* `pathValueStart`, and report where its metadata siblings are *now*.
|
|
@@ -3423,6 +3505,24 @@ async function resolveValCompletion({
|
|
|
3423
3505
|
*/
|
|
3424
3506
|
async function readMetadata(data) {
|
|
3425
3507
|
try {
|
|
3508
|
+
if (data.mediaType === "video") {
|
|
3509
|
+
// Only what could be read: a video whose duration cannot be read still
|
|
3510
|
+
// gets its mime type, and `video:add-metadata` reports the rest. From
|
|
3511
|
+
// the file rather than a buffer: only its headers are read.
|
|
3512
|
+
const metadata = await server.extractVideoMetadataFromFile(data.filePath);
|
|
3513
|
+
const rendered = {};
|
|
3514
|
+
if (metadata.width !== undefined && metadata.height !== undefined) {
|
|
3515
|
+
rendered.width = String(metadata.width);
|
|
3516
|
+
rendered.height = String(metadata.height);
|
|
3517
|
+
}
|
|
3518
|
+
if (metadata.mimeType) {
|
|
3519
|
+
rendered.mimeType = JSON.stringify(metadata.mimeType);
|
|
3520
|
+
}
|
|
3521
|
+
if (metadata.duration !== undefined) {
|
|
3522
|
+
rendered.duration = String(metadata.duration);
|
|
3523
|
+
}
|
|
3524
|
+
return Object.keys(rendered).length > 0 ? rendered : undefined;
|
|
3525
|
+
}
|
|
3426
3526
|
const buffer = fs__default["default"].readFileSync(data.filePath);
|
|
3427
3527
|
if (data.mediaType === "image") {
|
|
3428
3528
|
const metadata = await server.extractImageMetadata(data.filePath, buffer);
|
|
@@ -3497,7 +3597,10 @@ function createSchemaDrivenCompletions({
|
|
|
3497
3597
|
return [];
|
|
3498
3598
|
}
|
|
3499
3599
|
const directory = "dir" in container && typeof container.dir === "string" ? container.dir : undefined;
|
|
3500
|
-
const galleryFiles = container.mediaType === "images" ? files.images(directory) :
|
|
3600
|
+
const galleryFiles = container.mediaType === "images" ? files.images(directory) : container.mediaType === "videos" ?
|
|
3601
|
+
// A stream is keyed by its master playlist; its media playlists
|
|
3602
|
+
// and segments are part of it, never keys of their own.
|
|
3603
|
+
files.videos(directory) : files.list(directory);
|
|
3501
3604
|
return items(galleryFiles.map(file => file.ref), vscodeLanguageserver.CompletionItemKind.File, range);
|
|
3502
3605
|
}
|
|
3503
3606
|
|
|
@@ -3509,7 +3612,7 @@ function createSchemaDrivenCompletions({
|
|
|
3509
3612
|
// was ignored.
|
|
3510
3613
|
if (!isPropertyName && valueOfProperty === "path") {
|
|
3511
3614
|
const container = resolveSchemaAt(parentModulePath(modulePath), source, schema);
|
|
3512
|
-
if (container && typeof container === "object" && "type" in container && (container.type === "image" || container.type === "file")) {
|
|
3615
|
+
if (container && typeof container === "object" && "type" in container && (container.type === "image" || container.type === "file" || container.type === "video")) {
|
|
3513
3616
|
return mediaPathItems({
|
|
3514
3617
|
document,
|
|
3515
3618
|
container,
|
|
@@ -3569,7 +3672,7 @@ function mediaPathItems({
|
|
|
3569
3672
|
directory = gallery.dir;
|
|
3570
3673
|
}
|
|
3571
3674
|
}
|
|
3572
|
-
const candidates = container.type === "image" ? files.images(directory) : files.list(directory);
|
|
3675
|
+
const candidates = container.type === "image" ? files.images(directory) : container.type === "video" ? files.videos(directory) : files.list(directory);
|
|
3573
3676
|
return candidates.map((file, index) => {
|
|
3574
3677
|
const data = {
|
|
3575
3678
|
kind: "media-path",
|
|
@@ -3806,7 +3909,11 @@ async function createGalleryMembershipActions({
|
|
|
3806
3909
|
if (register) {
|
|
3807
3910
|
actions.push(register);
|
|
3808
3911
|
}
|
|
3809
|
-
|
|
3912
|
+
|
|
3913
|
+
// A stream is a directory of files its master playlist names, and renaming
|
|
3914
|
+
// the master alone would leave every one of them behind.
|
|
3915
|
+
const isStream = gallery.mediaType === "video" && gallery.path.split("?")[0].toLowerCase().endsWith(".m3u8");
|
|
3916
|
+
if (allowRename && gallery.dir && onDisk && !isStream) {
|
|
3810
3917
|
const move = createMoveIntoGalleryDirectoryAction({
|
|
3811
3918
|
document,
|
|
3812
3919
|
gallery,
|
|
@@ -3859,7 +3966,7 @@ async function createRegisterInGalleryAction({
|
|
|
3859
3966
|
},
|
|
3860
3967
|
newText: insertion.hasProperties ? `,\n${insertion.indentation}${entry}` : `\n${insertion.indentation}${entry}\n${insertion.indentation.slice(2)}`
|
|
3861
3968
|
};
|
|
3862
|
-
return vscodeLanguageserver.CodeAction.create(`Val: add ${path__default["default"].posix.basename(gallery.path)} to the gallery`, {
|
|
3969
|
+
return vscodeLanguageserver.CodeAction.create(`Val: add ${path__default["default"].posix.basename(gallery.path)} to the ${gallery.mediaType === "video" ? "video set" : "gallery"}`, {
|
|
3863
3970
|
changes: {
|
|
3864
3971
|
[pathToUri(galleryFile)]: [edit]
|
|
3865
3972
|
}
|
|
@@ -3981,6 +4088,16 @@ function findRecordInsertion(sourceFile) {
|
|
|
3981
4088
|
*/
|
|
3982
4089
|
async function readMetadataSource(filePath, mediaType) {
|
|
3983
4090
|
try {
|
|
4091
|
+
if (mediaType === "video") {
|
|
4092
|
+
// All four or nothing: a set entry missing any of them is reported by
|
|
4093
|
+
// `videos:add-metadata`, which says why it could not be read — a better
|
|
4094
|
+
// place to land than a registered entry that is already wrong.
|
|
4095
|
+
const metadata = await server.extractVideoMetadataFromFile(filePath);
|
|
4096
|
+
if (!metadata.mimeType || metadata.width === undefined || metadata.height === undefined || metadata.duration === undefined) {
|
|
4097
|
+
return undefined;
|
|
4098
|
+
}
|
|
4099
|
+
return `mimeType: ${JSON.stringify(metadata.mimeType)}, width: ${metadata.width}, height: ${metadata.height}, duration: ${metadata.duration}, alt: null`;
|
|
4100
|
+
}
|
|
3984
4101
|
const buffer = fs__default["default"].readFileSync(filePath);
|
|
3985
4102
|
if (mediaType === "image") {
|
|
3986
4103
|
const metadata = await server.extractImageMetadata(filePath, buffer);
|
|
@@ -4009,12 +4126,12 @@ function readFileOrUndefined(filePath) {
|
|
|
4009
4126
|
/**
|
|
4010
4127
|
* Lists the files a Val module may reference.
|
|
4011
4128
|
*
|
|
4012
|
-
* Val stores referenced files under `/public/val` by convention
|
|
4013
|
-
*
|
|
4014
|
-
*
|
|
4129
|
+
* Val stores referenced files under `/public/val` by convention, and a
|
|
4130
|
+
* reference is written as the path *including* the `/public` prefix — so this
|
|
4131
|
+
* returns Val-style refs directly.
|
|
4015
4132
|
*/
|
|
4016
4133
|
|
|
4017
|
-
/**
|
|
4134
|
+
/** Where Val stores the files a module references. */
|
|
4018
4135
|
const DEFAULT_FILES_DIRECTORY = "/public/val";
|
|
4019
4136
|
|
|
4020
4137
|
/**
|
|
@@ -4085,6 +4202,7 @@ function createPublicValFiles({
|
|
|
4085
4202
|
return {
|
|
4086
4203
|
list,
|
|
4087
4204
|
images: dir => list(dir).filter(f => f.mimeType?.startsWith("image/")),
|
|
4205
|
+
videos: dir => list(dir).filter(f => f.mimeType?.startsWith("video/") || path__default["default"].posix.basename(f.ref) === "master.m3u8"),
|
|
4088
4206
|
invalidate: () => {
|
|
4089
4207
|
cache.clear();
|
|
4090
4208
|
}
|
|
@@ -4451,24 +4569,6 @@ function createValLanguageServer(connection) {
|
|
|
4451
4569
|
publicFiles = createPublicValFiles({
|
|
4452
4570
|
valRoot: options.valRoot
|
|
4453
4571
|
});
|
|
4454
|
-
// A project can point `files.directory` somewhere other than /public/val, and
|
|
4455
|
-
// media-path completions would list nothing if we assumed the default.
|
|
4456
|
-
// Reading val.config is async and `initialize` is not, so the default stands
|
|
4457
|
-
// until the real value arrives — completions cannot be requested before
|
|
4458
|
-
// `initialize` returns anyway.
|
|
4459
|
-
void server.findAndEvalValConfigFile(options.valRoot).then(config => {
|
|
4460
|
-
const directory = config?.files?.directory;
|
|
4461
|
-
if (directory && directory !== DEFAULT_FILES_DIRECTORY) {
|
|
4462
|
-
publicFiles = createPublicValFiles({
|
|
4463
|
-
valRoot: options.valRoot,
|
|
4464
|
-
directory
|
|
4465
|
-
});
|
|
4466
|
-
}
|
|
4467
|
-
}).catch(e => {
|
|
4468
|
-
// A broken or unreadable val.config is reported per module by the
|
|
4469
|
-
// service; here it only means "keep the default directory".
|
|
4470
|
-
connection.console.warn(`Val: could not read val.config: ${e instanceof Error ? e.message : String(e)}`);
|
|
4471
|
-
});
|
|
4472
4572
|
project = createValProject({
|
|
4473
4573
|
valRoot: options.valRoot,
|
|
4474
4574
|
open: {
|