@staticbolt/lsp 1.0.0-beta.12
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/LICENSE +22 -0
- package/README.md +1 -0
- package/lib/index.d.mts +1 -0
- package/lib/index.mjs +903 -0
- package/lib/index.mjs.map +1 -0
- package/package.json +35 -0
- package/src/helpers/config-loader.ts +232 -0
- package/src/helpers/find-projects.ts +23 -0
- package/src/html-server.ts +200 -0
- package/src/index.ts +37 -0
- package/src/language-model-cache.ts +89 -0
- package/src/modes/embedded-support.ts +268 -0
- package/src/modes/html-mode.ts +99 -0
- package/src/modes/language-modes.ts +217 -0
- package/src/requests.ts +72 -0
- package/src/utils/arrays.ts +72 -0
- package/src/utils/document-context.ts +44 -0
- package/src/utils/find-project-root.ts +24 -0
- package/src/utils/node-fs.ts +79 -0
- package/src/utils/runner.ts +56 -0
- package/src/utils/strings.ts +76 -0
package/src/requests.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { RequestType } from "vscode-languageserver";
|
|
2
|
+
|
|
3
|
+
import type { RuntimeEnvironment } from "./html-server.ts";
|
|
4
|
+
import type { Connection } from "vscode-languageserver";
|
|
5
|
+
|
|
6
|
+
// Interface
|
|
7
|
+
export const FsStatRequest = { type: new RequestType<string, FileStat, unknown>("fs/stat") } as const;
|
|
8
|
+
|
|
9
|
+
// Interface
|
|
10
|
+
export const FsReadDirectoryRequest = { type: new RequestType<string, [string, FileType][], unknown>("fs/readDir") } as const;
|
|
11
|
+
|
|
12
|
+
export const FileType = Object.freeze({
|
|
13
|
+
/** The file type is unknown. */
|
|
14
|
+
Unknown: 0,
|
|
15
|
+
|
|
16
|
+
/** A regular file. */
|
|
17
|
+
File: 1,
|
|
18
|
+
|
|
19
|
+
/** A directory. */
|
|
20
|
+
Directory: 2,
|
|
21
|
+
|
|
22
|
+
/** A symbolic link to a file. */
|
|
23
|
+
SymbolicLink: 64,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type FileType = (typeof FileType)[keyof typeof FileType];
|
|
27
|
+
|
|
28
|
+
export interface FileStat {
|
|
29
|
+
/** The type of the file, e.g. is a regular file, a directory, or symbolic link to a file. */
|
|
30
|
+
type: FileType;
|
|
31
|
+
|
|
32
|
+
/** The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. */
|
|
33
|
+
ctime: number;
|
|
34
|
+
|
|
35
|
+
/** The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. */
|
|
36
|
+
mtime: number;
|
|
37
|
+
|
|
38
|
+
/** The size in bytes. */
|
|
39
|
+
size: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FileSystemProvider {
|
|
43
|
+
stat(uri: string): Promise<FileStat>;
|
|
44
|
+
readDirectory(uri: string): Promise<[string, FileType][]>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getFileSystemProvider(
|
|
48
|
+
handledSchemas: string[],
|
|
49
|
+
connection: Connection,
|
|
50
|
+
runtime: RuntimeEnvironment
|
|
51
|
+
): FileSystemProvider {
|
|
52
|
+
const fileFs = runtime.fileFs && handledSchemas.includes("file") ? runtime.fileFs : undefined;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
async stat(uri: string): Promise<FileStat> {
|
|
56
|
+
if (fileFs && uri.startsWith("file:")) {
|
|
57
|
+
return fileFs.stat(uri);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = await connection.sendRequest(FsStatRequest.type, uri.toString());
|
|
61
|
+
return result;
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
readDirectory(uri: string): Promise<[string, FileType][]> {
|
|
65
|
+
if (fileFs && uri.startsWith("file:")) {
|
|
66
|
+
return fileFs.readDirectory(uri);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return connection.sendRequest(FsReadDirectoryRequest.type, uri.toString());
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function pushAll<T>(to: T[], from: T[]) {
|
|
2
|
+
if (from) {
|
|
3
|
+
for (const item of from) {
|
|
4
|
+
to.push(item);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function contains<T>(array: T[], value: T) {
|
|
10
|
+
return array.includes(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` so only use this when actually needing
|
|
15
|
+
* stable sort.
|
|
16
|
+
*/
|
|
17
|
+
export function mergeSort<T>(data: T[], compare: (a: T, b: T) => number): T[] {
|
|
18
|
+
_divideAndMerge(data, compare);
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function _divideAndMerge<T>(data: T[], compare: (a: T, b: T) => number): void {
|
|
23
|
+
if (data.length <= 1) {
|
|
24
|
+
// sorted
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const p = Math.trunc(data.length / 2);
|
|
28
|
+
const left = data.slice(0, p);
|
|
29
|
+
const right = data.slice(p);
|
|
30
|
+
|
|
31
|
+
_divideAndMerge(left, compare);
|
|
32
|
+
_divideAndMerge(right, compare);
|
|
33
|
+
|
|
34
|
+
let leftIndex = 0;
|
|
35
|
+
let rightIndex = 0;
|
|
36
|
+
let index = 0;
|
|
37
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
38
|
+
const returnValue = compare(left[leftIndex], right[rightIndex]);
|
|
39
|
+
if (returnValue <= 0) {
|
|
40
|
+
// smaller_equal -> take left to preserve order
|
|
41
|
+
data[index++] = left[leftIndex++];
|
|
42
|
+
} else {
|
|
43
|
+
// greater -> take right
|
|
44
|
+
data[index++] = right[rightIndex++];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
while (leftIndex < left.length) {
|
|
48
|
+
data[index++] = left[leftIndex++];
|
|
49
|
+
}
|
|
50
|
+
while (rightIndex < right.length) {
|
|
51
|
+
data[index++] = right[rightIndex++];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function binarySearch<T>(array: T[], key: T, comparator: (op1: T, op2: T) => number): number {
|
|
56
|
+
let low = 0,
|
|
57
|
+
high = array.length - 1;
|
|
58
|
+
|
|
59
|
+
while (low <= high) {
|
|
60
|
+
const mid = Math.trunc((low + high) / 2);
|
|
61
|
+
const comp = comparator(array[mid], key);
|
|
62
|
+
if (comp < 0) {
|
|
63
|
+
low = mid + 1;
|
|
64
|
+
} else if (comp > 0) {
|
|
65
|
+
high = mid - 1;
|
|
66
|
+
} else {
|
|
67
|
+
return mid;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return -(low + 1);
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as vscodeUri from "vscode-uri";
|
|
2
|
+
|
|
3
|
+
import { endsWith, startsWith } from "../utils/strings.ts";
|
|
4
|
+
|
|
5
|
+
import type { DocumentContext } from "vscode-css-languageservice";
|
|
6
|
+
import type { WorkspaceFolder } from "vscode-languageserver";
|
|
7
|
+
|
|
8
|
+
export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext {
|
|
9
|
+
function getRootFolder(): string | undefined {
|
|
10
|
+
for (const folder of workspaceFolders) {
|
|
11
|
+
let folderURI = folder.uri;
|
|
12
|
+
if (!endsWith(folderURI, "/")) {
|
|
13
|
+
folderURI = folderURI + "/";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (startsWith(documentUri, folderURI)) {
|
|
17
|
+
return folderURI;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
resolveReference: (reference: string, base = documentUri) => {
|
|
26
|
+
if (/^\w[\w\d+.-]*:/.test(reference)) {
|
|
27
|
+
// starts with a schema
|
|
28
|
+
return reference;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (reference[0] === "/") {
|
|
32
|
+
// resolve absolute path against the current workspace folder
|
|
33
|
+
const folderUri = getRootFolder();
|
|
34
|
+
if (folderUri) {
|
|
35
|
+
return folderUri + reference.slice(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const baseUri = vscodeUri.URI.parse(base);
|
|
40
|
+
const baseUriDirectory = baseUri.path.endsWith("/") ? baseUri : vscodeUri.Utils.dirname(baseUri);
|
|
41
|
+
return vscodeUri.Utils.resolvePath(baseUriDirectory, reference).toString(true);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as vscodeUri from "vscode-uri";
|
|
4
|
+
|
|
5
|
+
export function findProjectRoot(fileUri: string): string | null {
|
|
6
|
+
let directory = path.dirname(vscodeUri.URI.parse(fileUri).fsPath);
|
|
7
|
+
|
|
8
|
+
while (true) {
|
|
9
|
+
for (const config of [".staticbolt.ts", ".staticbolt.js"]) {
|
|
10
|
+
if (fs.existsSync(path.join(directory, config))) {
|
|
11
|
+
return directory;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const parent = path.dirname(directory);
|
|
16
|
+
|
|
17
|
+
// reached fs root, no config found
|
|
18
|
+
if (parent === directory) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
directory = parent;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as vscodeUri from "vscode-uri";
|
|
3
|
+
|
|
4
|
+
import type { FileSystemProvider } from "../requests.ts";
|
|
5
|
+
|
|
6
|
+
const FileType = Object.freeze({
|
|
7
|
+
/** The file type is unknown. */
|
|
8
|
+
Unknown: 0,
|
|
9
|
+
/** A regular file. */
|
|
10
|
+
File: 1,
|
|
11
|
+
/** A directory. */
|
|
12
|
+
Directory: 2,
|
|
13
|
+
/** A symbolic link to a file. */
|
|
14
|
+
SymbolicLink: 64,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
type FileType = (typeof FileType)[keyof typeof FileType];
|
|
18
|
+
|
|
19
|
+
export function getNodeFileFS(): FileSystemProvider {
|
|
20
|
+
function ensureFileUri(location: string) {
|
|
21
|
+
if (!location.startsWith("file:")) {
|
|
22
|
+
throw new Error("fileSystemProvider can only handle file URLs");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
stat(location: string) {
|
|
27
|
+
ensureFileUri(location);
|
|
28
|
+
return new Promise((c, error_) => {
|
|
29
|
+
const uri = vscodeUri.URI.parse(location);
|
|
30
|
+
fs.stat(uri.fsPath, (error, stats) => {
|
|
31
|
+
if (error) {
|
|
32
|
+
return error.code === "ENOENT" ? c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 }) : error_(error);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let type: FileType = FileType.Unknown;
|
|
36
|
+
if (stats.isFile()) {
|
|
37
|
+
type = FileType.File;
|
|
38
|
+
} else if (stats.isDirectory()) {
|
|
39
|
+
type = FileType.Directory;
|
|
40
|
+
} else if (stats.isSymbolicLink()) {
|
|
41
|
+
type = FileType.SymbolicLink;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
c({
|
|
45
|
+
type,
|
|
46
|
+
ctime: stats.ctime.getTime(),
|
|
47
|
+
mtime: stats.mtime.getTime(),
|
|
48
|
+
size: stats.size,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
readDirectory(location: string) {
|
|
54
|
+
ensureFileUri(location);
|
|
55
|
+
return new Promise((c, error_) => {
|
|
56
|
+
const path = vscodeUri.URI.parse(location).fsPath;
|
|
57
|
+
|
|
58
|
+
fs.readdir(path, { withFileTypes: true }, (error, children) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
return error_(error);
|
|
61
|
+
}
|
|
62
|
+
c(
|
|
63
|
+
children.map(stat => {
|
|
64
|
+
if (stat.isSymbolicLink()) {
|
|
65
|
+
return [stat.name, FileType.SymbolicLink];
|
|
66
|
+
} else if (stat.isDirectory()) {
|
|
67
|
+
return [stat.name, FileType.Directory];
|
|
68
|
+
} else if (stat.isFile()) {
|
|
69
|
+
return [stat.name, FileType.File];
|
|
70
|
+
} else {
|
|
71
|
+
return [stat.name, FileType.Unknown];
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import vscode from "vscode-languageserver";
|
|
2
|
+
|
|
3
|
+
import type { RuntimeEnvironment } from "../html-server.ts";
|
|
4
|
+
import type { CancellationToken, ResponseError } from "vscode-languageserver";
|
|
5
|
+
|
|
6
|
+
export function formatError(message: string, error: unknown): string {
|
|
7
|
+
if (error instanceof Error) {
|
|
8
|
+
return `${message}: ${error.message}\n${error.stack}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof error === "string") {
|
|
12
|
+
return `${message}: ${error}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (error) {
|
|
16
|
+
return `${message}: ${(error as string).toString()}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return message;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runSafe<T>(
|
|
23
|
+
runtime: RuntimeEnvironment,
|
|
24
|
+
function_: () => Thenable<T>,
|
|
25
|
+
errorValue: T,
|
|
26
|
+
errorMessage: string,
|
|
27
|
+
token: CancellationToken
|
|
28
|
+
): Thenable<T | ResponseError<never>> {
|
|
29
|
+
return new Promise<T | ResponseError<never>>(resolve => {
|
|
30
|
+
runtime.timer.setImmediate(() => {
|
|
31
|
+
if (token.isCancellationRequested) {
|
|
32
|
+
resolve(cancelValue());
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
return function_().then(
|
|
36
|
+
result => {
|
|
37
|
+
if (token.isCancellationRequested) {
|
|
38
|
+
resolve(cancelValue());
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resolve(result);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
error => {
|
|
46
|
+
console.error(formatError(errorMessage, error));
|
|
47
|
+
resolve(errorValue);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cancelValue<E>() {
|
|
55
|
+
return new vscode.ResponseError<E>(vscode.LSPErrorCodes.RequestCancelled, "Request cancelled");
|
|
56
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export function getWordAtText(text: string, offset: number, wordDefinition: RegExp): { start: number; length: number } {
|
|
2
|
+
let lineStart = offset;
|
|
3
|
+
while (lineStart > 0 && !isNewlineCharacter(text.codePointAt(lineStart - 1)!)) {
|
|
4
|
+
lineStart--;
|
|
5
|
+
}
|
|
6
|
+
const offsetInLine = offset - lineStart;
|
|
7
|
+
const lineText = text.slice(lineStart);
|
|
8
|
+
|
|
9
|
+
// make a copy of the regex as to not keep the state
|
|
10
|
+
const flags = wordDefinition.ignoreCase ? "gi" : "g";
|
|
11
|
+
wordDefinition = new RegExp(wordDefinition.source, flags);
|
|
12
|
+
|
|
13
|
+
let match = wordDefinition.exec(lineText);
|
|
14
|
+
while (match && match.index + match[0].length < offsetInLine) {
|
|
15
|
+
match = wordDefinition.exec(lineText);
|
|
16
|
+
}
|
|
17
|
+
if (match && match.index <= offsetInLine) {
|
|
18
|
+
return { start: match.index + lineStart, length: match[0].length };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { start: offset, length: 0 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function startsWith(haystack: string, needle: string): boolean {
|
|
25
|
+
if (haystack.length < needle.length) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line unicorn/no-for-loop
|
|
30
|
+
for (let index = 0; index < needle.length; index++) {
|
|
31
|
+
if (haystack[index] !== needle[index]) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function endsWith(haystack: string, needle: string): boolean {
|
|
40
|
+
const diff = haystack.length - needle.length;
|
|
41
|
+
if (diff > 0) {
|
|
42
|
+
return haystack.indexOf(needle, diff) === diff;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (diff === 0) {
|
|
46
|
+
return haystack === needle;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function repeat(value: string, count: number) {
|
|
53
|
+
let s = "";
|
|
54
|
+
while (count > 0) {
|
|
55
|
+
if ((count & 1) === 1) {
|
|
56
|
+
s += value;
|
|
57
|
+
}
|
|
58
|
+
value += value;
|
|
59
|
+
count = count >>> 1;
|
|
60
|
+
}
|
|
61
|
+
return s;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isWhitespaceOnly(string_: string) {
|
|
65
|
+
return /^\s*$/.test(string_);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function isEOL(content: string, offset: number) {
|
|
69
|
+
return isNewlineCharacter(content.codePointAt(offset)!);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const CR = "\r".codePointAt(0);
|
|
73
|
+
const NL = "\n".codePointAt(0);
|
|
74
|
+
export function isNewlineCharacter(charCode: number) {
|
|
75
|
+
return charCode === CR || charCode === NL;
|
|
76
|
+
}
|