@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.
@@ -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
+ }