@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/index.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as vs from "vscode-languageserver/node";
|
|
2
|
+
|
|
3
|
+
import { startServer } from "./html-server.ts";
|
|
4
|
+
import { getNodeFileFS } from "./utils/node-fs.ts";
|
|
5
|
+
import { formatError } from "./utils/runner.ts";
|
|
6
|
+
|
|
7
|
+
import type { RuntimeEnvironment } from "./html-server.ts";
|
|
8
|
+
import type { Connection, Disposable } from "vscode-languageserver/node";
|
|
9
|
+
|
|
10
|
+
// Create a connection for the server.
|
|
11
|
+
const connection: Connection = vs.createConnection();
|
|
12
|
+
|
|
13
|
+
console.log = connection.console.log.bind(connection.console);
|
|
14
|
+
console.error = connection.console.error.bind(connection.console);
|
|
15
|
+
|
|
16
|
+
process.on("unhandledRejection", (error: unknown) => {
|
|
17
|
+
connection.console.error(formatError(`Unhandled exception`, error));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const runtime: RuntimeEnvironment = {
|
|
21
|
+
timer: {
|
|
22
|
+
setImmediate(callback: (...arguments_: unknown[]) => void, ...arguments_: unknown[]): Disposable {
|
|
23
|
+
const handle = setImmediate(callback, ...arguments_);
|
|
24
|
+
return { dispose: () => clearImmediate(handle) };
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
setTimeout(callback: (...arguments_: unknown[]) => void, ms: number, ...arguments_: unknown[]): Disposable {
|
|
28
|
+
const handle = setTimeout(callback, ms, ...arguments_);
|
|
29
|
+
return { dispose: () => clearTimeout(handle) };
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
fileFs: getNodeFileFS(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
connection.console.log("[staticbolt] starting server");
|
|
37
|
+
startServer(connection, runtime);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { TextDocument } from "vscode-html-languageservice";
|
|
2
|
+
|
|
3
|
+
export interface LanguageModelCache<T> {
|
|
4
|
+
get(document: TextDocument): T;
|
|
5
|
+
onDocumentRemoved(document: TextDocument): void;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getLanguageModelCache<T>(
|
|
10
|
+
maxEntries: number,
|
|
11
|
+
cleanupIntervalTimeInSec: number,
|
|
12
|
+
parse: (document: TextDocument) => T
|
|
13
|
+
): LanguageModelCache<T> {
|
|
14
|
+
let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {};
|
|
15
|
+
let nModels = 0;
|
|
16
|
+
|
|
17
|
+
let cleanupInterval: NodeJS.Timeout | undefined = undefined;
|
|
18
|
+
|
|
19
|
+
if (cleanupIntervalTimeInSec > 0) {
|
|
20
|
+
cleanupInterval = setInterval(() => {
|
|
21
|
+
const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000;
|
|
22
|
+
const uris = Object.keys(languageModels);
|
|
23
|
+
|
|
24
|
+
for (const uri of uris) {
|
|
25
|
+
const languageModelInfo = languageModels[uri];
|
|
26
|
+
if (languageModelInfo.cTime < cutoffTime) {
|
|
27
|
+
delete languageModels[uri];
|
|
28
|
+
nModels--;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}, cleanupIntervalTimeInSec * 1000);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
get(document: TextDocument): T {
|
|
36
|
+
const version = document.version;
|
|
37
|
+
const languageId = document.languageId;
|
|
38
|
+
const languageModelInfo = languageModels[document.uri];
|
|
39
|
+
|
|
40
|
+
if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) {
|
|
41
|
+
languageModelInfo.cTime = Date.now();
|
|
42
|
+
return languageModelInfo.languageModel;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const languageModel = parse(document);
|
|
46
|
+
languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() };
|
|
47
|
+
if (!languageModelInfo) {
|
|
48
|
+
nModels++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (nModels === maxEntries) {
|
|
52
|
+
let oldestTime = Number.MAX_VALUE;
|
|
53
|
+
let oldestUri = null;
|
|
54
|
+
|
|
55
|
+
for (const uri in languageModels) {
|
|
56
|
+
const languageModelInfo = languageModels[uri];
|
|
57
|
+
if (languageModelInfo.cTime < oldestTime) {
|
|
58
|
+
oldestUri = uri;
|
|
59
|
+
oldestTime = languageModelInfo.cTime;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (oldestUri) {
|
|
64
|
+
delete languageModels[oldestUri];
|
|
65
|
+
nModels--;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return languageModel;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
onDocumentRemoved(document: TextDocument) {
|
|
73
|
+
const uri = document.uri;
|
|
74
|
+
if (languageModels[uri]) {
|
|
75
|
+
delete languageModels[uri];
|
|
76
|
+
nModels--;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
dispose() {
|
|
81
|
+
if (cleanupInterval !== undefined) {
|
|
82
|
+
clearInterval(cleanupInterval);
|
|
83
|
+
cleanupInterval = undefined;
|
|
84
|
+
languageModels = {};
|
|
85
|
+
nModels = 0;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import vscodeHtml from "vscode-html-languageservice";
|
|
2
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
|
+
|
|
4
|
+
import type { LanguageService, Range } from "vscode-html-languageservice";
|
|
5
|
+
|
|
6
|
+
export interface LanguageRange extends Range {
|
|
7
|
+
languageId: string | undefined;
|
|
8
|
+
attributeValue?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface HTMLDocumentRegions {
|
|
12
|
+
getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument;
|
|
13
|
+
getLanguageRanges(range: Range): LanguageRange[];
|
|
14
|
+
getLanguageAtPosition(position: Position): string | undefined;
|
|
15
|
+
getLanguagesInDocument(): string[];
|
|
16
|
+
getImportedScripts(): string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CSS_STYLE_RULE = "__";
|
|
20
|
+
const TokenType = vscodeHtml.TokenType;
|
|
21
|
+
const Position = vscodeHtml.Position;
|
|
22
|
+
type Position = vscodeHtml.Position;
|
|
23
|
+
|
|
24
|
+
interface EmbeddedRegion {
|
|
25
|
+
languageId: string | undefined;
|
|
26
|
+
start: number;
|
|
27
|
+
end: number;
|
|
28
|
+
attributeValue?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getDocumentRegions(languageService: LanguageService, document: TextDocument): HTMLDocumentRegions {
|
|
32
|
+
const regions: EmbeddedRegion[] = [];
|
|
33
|
+
const scanner = languageService.createScanner(document.getText());
|
|
34
|
+
let lastTagName: string = "";
|
|
35
|
+
let lastAttributeName: string | null = null;
|
|
36
|
+
let languageIdFromType: string | undefined = undefined;
|
|
37
|
+
const importedScripts: string[] = [];
|
|
38
|
+
|
|
39
|
+
let token = scanner.scan();
|
|
40
|
+
|
|
41
|
+
while (token !== TokenType.EOS) {
|
|
42
|
+
switch (token) {
|
|
43
|
+
case TokenType.StartTag:
|
|
44
|
+
lastTagName = scanner.getTokenText();
|
|
45
|
+
lastAttributeName = null;
|
|
46
|
+
languageIdFromType = "javascript";
|
|
47
|
+
break;
|
|
48
|
+
case TokenType.Styles:
|
|
49
|
+
regions.push({ languageId: "css", start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
|
|
50
|
+
break;
|
|
51
|
+
case TokenType.Script:
|
|
52
|
+
regions.push({ languageId: languageIdFromType, start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
|
|
53
|
+
break;
|
|
54
|
+
case TokenType.AttributeName:
|
|
55
|
+
lastAttributeName = scanner.getTokenText();
|
|
56
|
+
break;
|
|
57
|
+
case TokenType.AttributeValue:
|
|
58
|
+
if (lastAttributeName === "src" && lastTagName.toLowerCase() === "script") {
|
|
59
|
+
let value = scanner.getTokenText();
|
|
60
|
+
if (value[0] === "'" || value[0] === '"') {
|
|
61
|
+
value = value.slice(1, -1);
|
|
62
|
+
}
|
|
63
|
+
importedScripts.push(value);
|
|
64
|
+
} else if (lastAttributeName === "type" && lastTagName.toLowerCase() === "script") {
|
|
65
|
+
const token = scanner.getTokenText();
|
|
66
|
+
if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(token) || token === "module") {
|
|
67
|
+
languageIdFromType = "javascript";
|
|
68
|
+
} else if (/["']text\/typescript["']/.test(token)) {
|
|
69
|
+
languageIdFromType = "typescript";
|
|
70
|
+
} else {
|
|
71
|
+
languageIdFromType = undefined;
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const attributeLanguageId = getAttributeLanguage(lastAttributeName!);
|
|
75
|
+
if (attributeLanguageId) {
|
|
76
|
+
let start = scanner.getTokenOffset();
|
|
77
|
+
let end = scanner.getTokenEnd();
|
|
78
|
+
const firstChar = document.getText()[start];
|
|
79
|
+
if (firstChar === "'" || firstChar === '"') {
|
|
80
|
+
start++;
|
|
81
|
+
end--;
|
|
82
|
+
}
|
|
83
|
+
regions.push({ languageId: attributeLanguageId, start, end, attributeValue: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lastAttributeName = null;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
token = scanner.scan();
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range),
|
|
93
|
+
getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) =>
|
|
94
|
+
getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues),
|
|
95
|
+
getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, regions, position),
|
|
96
|
+
getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
|
|
97
|
+
getImportedScripts: () => importedScripts,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getLanguageRanges(document: TextDocument, regions: EmbeddedRegion[], range: Range): LanguageRange[] {
|
|
102
|
+
const result: LanguageRange[] = [];
|
|
103
|
+
let currentPos = range ? range.start : Position.create(0, 0);
|
|
104
|
+
let currentOffset = range ? document.offsetAt(range.start) : 0;
|
|
105
|
+
const endOffset = range ? document.offsetAt(range.end) : document.getText().length;
|
|
106
|
+
for (const region of regions) {
|
|
107
|
+
if (region.end > currentOffset && region.start < endOffset) {
|
|
108
|
+
const start = Math.max(region.start, currentOffset);
|
|
109
|
+
const startPos = document.positionAt(start);
|
|
110
|
+
if (currentOffset < region.start) {
|
|
111
|
+
result.push({
|
|
112
|
+
start: currentPos,
|
|
113
|
+
end: startPos,
|
|
114
|
+
languageId: "html",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const end = Math.min(region.end, endOffset);
|
|
118
|
+
const endPos = document.positionAt(end);
|
|
119
|
+
if (end > region.start) {
|
|
120
|
+
result.push({
|
|
121
|
+
start: startPos,
|
|
122
|
+
end: endPos,
|
|
123
|
+
languageId: region.languageId,
|
|
124
|
+
attributeValue: region.attributeValue,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
currentOffset = end;
|
|
128
|
+
currentPos = endPos;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (currentOffset < endOffset) {
|
|
132
|
+
const endPos = range ? range.end : document.positionAt(endOffset);
|
|
133
|
+
result.push({
|
|
134
|
+
start: currentPos,
|
|
135
|
+
end: endPos,
|
|
136
|
+
languageId: "html",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getLanguagesInDocument(_document: TextDocument, regions: EmbeddedRegion[]): string[] {
|
|
143
|
+
const result: string[] = [];
|
|
144
|
+
|
|
145
|
+
for (const region of regions) {
|
|
146
|
+
if (region.languageId && !result.includes(region.languageId)) {
|
|
147
|
+
result.push(region.languageId);
|
|
148
|
+
if (result.length === 3) {
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
result.push("html");
|
|
155
|
+
|
|
156
|
+
return result;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined {
|
|
160
|
+
const offset = document.offsetAt(position);
|
|
161
|
+
for (const region of regions) {
|
|
162
|
+
if (region.start <= offset) {
|
|
163
|
+
if (offset <= region.end) {
|
|
164
|
+
return region.languageId;
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return "html";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getEmbeddedDocument(
|
|
174
|
+
document: TextDocument,
|
|
175
|
+
contents: EmbeddedRegion[],
|
|
176
|
+
languageId: string,
|
|
177
|
+
ignoreAttributeValues: boolean
|
|
178
|
+
): TextDocument {
|
|
179
|
+
let currentPos = 0;
|
|
180
|
+
const oldContent = document.getText();
|
|
181
|
+
let result = "";
|
|
182
|
+
let lastSuffix = "";
|
|
183
|
+
for (const c of contents) {
|
|
184
|
+
if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) {
|
|
185
|
+
result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c));
|
|
186
|
+
result += updateContent(c, oldContent.slice(c.start, c.end));
|
|
187
|
+
currentPos = c.end;
|
|
188
|
+
lastSuffix = getSuffix(c);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, "");
|
|
192
|
+
return TextDocument.create(document.uri, languageId, document.version, result);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getPrefix(c: EmbeddedRegion) {
|
|
196
|
+
if (c.attributeValue) {
|
|
197
|
+
switch (c.languageId) {
|
|
198
|
+
case "css":
|
|
199
|
+
return CSS_STYLE_RULE + "{";
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return "";
|
|
203
|
+
}
|
|
204
|
+
function getSuffix(c: EmbeddedRegion) {
|
|
205
|
+
if (c.attributeValue) {
|
|
206
|
+
switch (c.languageId) {
|
|
207
|
+
case "css":
|
|
208
|
+
return "}";
|
|
209
|
+
case "javascript":
|
|
210
|
+
return ";";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
function updateContent(c: EmbeddedRegion, content: string): string {
|
|
216
|
+
if (!c.attributeValue && c.languageId === "javascript") {
|
|
217
|
+
return content.replace(`<!--`, `/* `).replace(`-->`, ` */`);
|
|
218
|
+
}
|
|
219
|
+
if (c.languageId === "css") {
|
|
220
|
+
const quoteEscape = /("|")/g;
|
|
221
|
+
return content.replace(quoteEscape, (match, _, offset: number) => {
|
|
222
|
+
const spaces = " ".repeat(match.length - 1);
|
|
223
|
+
const afterChar = content[offset + match.length];
|
|
224
|
+
if (!afterChar || afterChar.includes(" ")) {
|
|
225
|
+
return `${spaces}"`;
|
|
226
|
+
}
|
|
227
|
+
return `"${spaces}`;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return content;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) {
|
|
234
|
+
result += before;
|
|
235
|
+
let accumulatedWS = -before.length; // start with a negative value to account for the before string
|
|
236
|
+
for (let index = start; index < end; index++) {
|
|
237
|
+
const ch = oldContent[index];
|
|
238
|
+
if (ch === "\n" || ch === "\r") {
|
|
239
|
+
// only write new lines, skip the whitespace
|
|
240
|
+
accumulatedWS = 0;
|
|
241
|
+
result += ch;
|
|
242
|
+
} else {
|
|
243
|
+
accumulatedWS++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
result = append(result, " ", accumulatedWS - after.length);
|
|
247
|
+
result += after;
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function append(result: string, string_: string, n: number): string {
|
|
252
|
+
while (n > 0) {
|
|
253
|
+
if (n & 1) {
|
|
254
|
+
result += string_;
|
|
255
|
+
}
|
|
256
|
+
n >>= 1;
|
|
257
|
+
string_ += string_;
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getAttributeLanguage(attributeName: string): string | null {
|
|
263
|
+
const match = attributeName.match(/^(style)$|^(on\w+)$/i);
|
|
264
|
+
if (!match) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return match[1] ? "css" : "javascript";
|
|
268
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/require-await */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Resolver } from "@staticbolt/core";
|
|
4
|
+
import * as vscodeUri from "vscode-uri";
|
|
5
|
+
|
|
6
|
+
import { getLanguageModelCache } from "../language-model-cache.ts";
|
|
7
|
+
import { FILE_PROTOCOL } from "./language-modes.ts";
|
|
8
|
+
|
|
9
|
+
import type { LanguageMode } from "./language-modes.ts";
|
|
10
|
+
import type { HTMLDataV1, HTMLDocument, LanguageService, TextDocument } from "vscode-html-languageservice";
|
|
11
|
+
|
|
12
|
+
export function getHTMLMode(htmlLanguageService: LanguageService): LanguageMode {
|
|
13
|
+
const htmlDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
getId() {
|
|
17
|
+
return "html";
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
async doComplete(document, position, documentContext, htmlData) {
|
|
21
|
+
setHtmlDataProviders(htmlLanguageService, htmlData);
|
|
22
|
+
|
|
23
|
+
const htmlDocument = htmlDocuments.get(document);
|
|
24
|
+
const completionList = await htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext);
|
|
25
|
+
|
|
26
|
+
for (const item of completionList.items) {
|
|
27
|
+
item.sortText = "0_" + item.label;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return completionList;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async doHover(document, position, htmlData) {
|
|
34
|
+
setHtmlDataProviders(htmlLanguageService, htmlData);
|
|
35
|
+
|
|
36
|
+
return htmlLanguageService.doHover(document, position, htmlDocuments.get(document));
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async onDocumentRemoved(document: TextDocument) {
|
|
40
|
+
htmlDocuments.onDocumentRemoved(document);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async findDocumentLinks(document, documentContext, projectRoot) {
|
|
44
|
+
const resolver = new Resolver(projectRoot);
|
|
45
|
+
const documentFs = vscodeUri.URI.parse(document.uri).fsPath;
|
|
46
|
+
const projectRootRelative = path.relative(projectRoot, documentFs);
|
|
47
|
+
|
|
48
|
+
const links = htmlLanguageService.findDocumentLinks(document, documentContext);
|
|
49
|
+
|
|
50
|
+
for (const link of links) {
|
|
51
|
+
if (!link.target) continue;
|
|
52
|
+
|
|
53
|
+
const linkFs = vscodeUri.URI.parse(link.target ?? "").fsPath;
|
|
54
|
+
const source = path.relative(path.dirname(documentFs), linkFs);
|
|
55
|
+
|
|
56
|
+
const resolved = resolver.resolve(source, projectRootRelative);
|
|
57
|
+
if (resolved) {
|
|
58
|
+
link.target = vscodeUri.URI.file(resolved.path).toString();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return links;
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
dispose() {
|
|
66
|
+
htmlDocuments.dispose();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function setHtmlDataProviders(htmlLanguageService: LanguageService, htmlData: HTMLDataV1[]) {
|
|
72
|
+
htmlLanguageService.setDataProviders(false, [
|
|
73
|
+
{
|
|
74
|
+
getId() {
|
|
75
|
+
return FILE_PROTOCOL;
|
|
76
|
+
},
|
|
77
|
+
isApplicable(languageId) {
|
|
78
|
+
return languageId === "html";
|
|
79
|
+
},
|
|
80
|
+
provideValues(tag, attribute) {
|
|
81
|
+
const tags = htmlData.flatMap(data => data.tags?.filter(t => t.name === tag) ?? []);
|
|
82
|
+
const attributes = tags.flatMap(t => t.attributes?.filter(a => a.name === attribute) ?? []);
|
|
83
|
+
const globalAttributes = htmlData.flatMap(data => data.globalAttributes?.filter(a => a.name === attribute) ?? []);
|
|
84
|
+
const values = [...globalAttributes, ...attributes].flatMap(a => a.values ?? []);
|
|
85
|
+
return values;
|
|
86
|
+
},
|
|
87
|
+
provideTags() {
|
|
88
|
+
const tags = htmlData.flatMap(data => data.tags ?? []);
|
|
89
|
+
return tags;
|
|
90
|
+
},
|
|
91
|
+
provideAttributes(tag: string) {
|
|
92
|
+
const tags = htmlData.flatMap(data => data.tags?.filter(t => t.name === tag) ?? []);
|
|
93
|
+
const globalAttributes = htmlData.flatMap(data => data.globalAttributes ?? []);
|
|
94
|
+
const attributes = tags.flatMap(t => t.attributes ?? []);
|
|
95
|
+
return [...globalAttributes, ...attributes];
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import vscodeHtml from "vscode-html-languageservice";
|
|
2
|
+
|
|
3
|
+
import { getLanguageModelCache } from "../language-model-cache.ts";
|
|
4
|
+
import { getDocumentRegions } from "./embedded-support.ts";
|
|
5
|
+
import { getHTMLMode } from "./html-mode.ts";
|
|
6
|
+
|
|
7
|
+
import type { LanguageModelCache } from "../language-model-cache.ts";
|
|
8
|
+
import type { FileSystemProvider } from "../requests.ts";
|
|
9
|
+
import type { HTMLDocumentRegions } from "./embedded-support.ts";
|
|
10
|
+
import type {
|
|
11
|
+
ClientCapabilities,
|
|
12
|
+
CompletionConfiguration,
|
|
13
|
+
DocumentContext,
|
|
14
|
+
HoverSettings,
|
|
15
|
+
HTMLDataV1,
|
|
16
|
+
IHTMLDataProvider,
|
|
17
|
+
} from "vscode-html-languageservice";
|
|
18
|
+
import type {
|
|
19
|
+
CompletionItem,
|
|
20
|
+
CompletionList,
|
|
21
|
+
DocumentLink,
|
|
22
|
+
Hover,
|
|
23
|
+
Position,
|
|
24
|
+
Range,
|
|
25
|
+
WorkspaceFolder,
|
|
26
|
+
} from "vscode-languageserver";
|
|
27
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
28
|
+
|
|
29
|
+
export interface Settings {
|
|
30
|
+
hover?: HoverSettings;
|
|
31
|
+
autoClosingTags?: boolean;
|
|
32
|
+
completion?: CompletionConfiguration;
|
|
33
|
+
suggest?: CompletionConfiguration;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Workspace {
|
|
37
|
+
readonly settings: Settings;
|
|
38
|
+
readonly folders: WorkspaceFolder[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SemanticTokenData {
|
|
42
|
+
start: Position;
|
|
43
|
+
length: number;
|
|
44
|
+
typeIdx: number;
|
|
45
|
+
modifierSet: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type CompletionItemData = {
|
|
49
|
+
languageId: string;
|
|
50
|
+
uri: string;
|
|
51
|
+
offset: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function isCompletionItemData(value: Record<string, unknown>): value is CompletionItemData {
|
|
55
|
+
return value && typeof value.languageId === "string" && typeof value.uri === "string" && typeof value.offset === "number";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LanguageMode {
|
|
59
|
+
getId(): string;
|
|
60
|
+
doComplete?: (
|
|
61
|
+
document: TextDocument,
|
|
62
|
+
position: Position,
|
|
63
|
+
documentContext: DocumentContext,
|
|
64
|
+
htmlData: HTMLDataV1[]
|
|
65
|
+
) => Promise<CompletionList>;
|
|
66
|
+
doResolve?: (document: TextDocument, item: CompletionItem) => Promise<CompletionItem>;
|
|
67
|
+
doHover?: (document: TextDocument, position: Position, htmlData: HTMLDataV1[]) => Promise<Hover | null>;
|
|
68
|
+
findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext, projectRoot: string) => Promise<DocumentLink[]>;
|
|
69
|
+
onDocumentRemoved(document: TextDocument): void;
|
|
70
|
+
dispose(): void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LanguageModes {
|
|
74
|
+
updateDataProviders(dataProviders: IHTMLDataProvider[]): void;
|
|
75
|
+
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined;
|
|
76
|
+
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[];
|
|
77
|
+
getAllModes(): LanguageMode[];
|
|
78
|
+
getAllModesInDocument(document: TextDocument): LanguageMode[];
|
|
79
|
+
getMode(languageId: string): LanguageMode | undefined;
|
|
80
|
+
onDocumentRemoved(document: TextDocument): void;
|
|
81
|
+
dispose(): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface LanguageModeRange extends Range {
|
|
85
|
+
mode: LanguageMode | undefined;
|
|
86
|
+
attributeValue?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const FILE_PROTOCOL = "staticbolt-server";
|
|
90
|
+
|
|
91
|
+
export function getLanguageModes(clientCapabilities: ClientCapabilities, requestService: FileSystemProvider): LanguageModes {
|
|
92
|
+
const htmlLanguageService = vscodeHtml.getLanguageService({
|
|
93
|
+
clientCapabilities,
|
|
94
|
+
fileSystemProvider: requestService,
|
|
95
|
+
customDataProviders: [
|
|
96
|
+
{
|
|
97
|
+
getId() {
|
|
98
|
+
return FILE_PROTOCOL;
|
|
99
|
+
},
|
|
100
|
+
isApplicable(languageId) {
|
|
101
|
+
return languageId === "html";
|
|
102
|
+
},
|
|
103
|
+
provideValues(tag, attribute) {
|
|
104
|
+
return [{ name: `tag:${tag} attribute:${attribute}` }];
|
|
105
|
+
},
|
|
106
|
+
provideTags() {
|
|
107
|
+
return [{ name: "staticbolt", description: "# staticbolt-description", attributes: [] }];
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
provideAttributes(tag: string) {
|
|
111
|
+
if (tag === "staticbolt") {
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
name: "staticbolt-attribute",
|
|
115
|
+
description: "# staticbolt-attribute-description",
|
|
116
|
+
values: [{ name: "staticbolt-attribute-value" }],
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [{ name: "global-attribute" }];
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
useDefaultDataProvider: false,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const documentRegions = getLanguageModelCache<HTMLDocumentRegions>(10, 60, document =>
|
|
129
|
+
getDocumentRegions(htmlLanguageService, document)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
let modelCaches: LanguageModelCache<unknown>[] = [documentRegions];
|
|
133
|
+
|
|
134
|
+
let modes = Object.create(null) as { [languageId: string]: LanguageMode };
|
|
135
|
+
modes["html"] = getHTMLMode(htmlLanguageService);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
139
|
+
async updateDataProviders(dataProviders: IHTMLDataProvider[]): Promise<void> {
|
|
140
|
+
htmlLanguageService.setDataProviders(true, dataProviders);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined {
|
|
144
|
+
const languageId = documentRegions.get(document).getLanguageAtPosition(position);
|
|
145
|
+
if (languageId) {
|
|
146
|
+
return modes[languageId];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return undefined;
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[] {
|
|
153
|
+
return documentRegions
|
|
154
|
+
.get(document)
|
|
155
|
+
.getLanguageRanges(range)
|
|
156
|
+
.map((r): LanguageModeRange => {
|
|
157
|
+
return {
|
|
158
|
+
start: r.start,
|
|
159
|
+
end: r.end,
|
|
160
|
+
mode: r.languageId ? modes[r.languageId] : undefined,
|
|
161
|
+
attributeValue: r.attributeValue,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
getAllModesInDocument(document: TextDocument): LanguageMode[] {
|
|
167
|
+
const result: LanguageMode[] = [];
|
|
168
|
+
for (const languageId of documentRegions.get(document).getLanguagesInDocument()) {
|
|
169
|
+
const mode = modes[languageId];
|
|
170
|
+
if (mode) {
|
|
171
|
+
result.push(mode);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
getAllModes(): LanguageMode[] {
|
|
179
|
+
const result = [];
|
|
180
|
+
for (const languageId in modes) {
|
|
181
|
+
const mode = modes[languageId];
|
|
182
|
+
if (mode) {
|
|
183
|
+
result.push(mode);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
getMode(languageId: string): LanguageMode {
|
|
191
|
+
return modes[languageId];
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
onDocumentRemoved(document: TextDocument) {
|
|
195
|
+
for (const mc of modelCaches) {
|
|
196
|
+
mc.onDocumentRemoved(document);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const mode in modes) {
|
|
200
|
+
modes[mode].onDocumentRemoved(document);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
dispose(): void {
|
|
205
|
+
for (const mc of modelCaches) {
|
|
206
|
+
mc.dispose();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
modelCaches = [];
|
|
210
|
+
for (const mode in modes) {
|
|
211
|
+
modes[mode].dispose();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
modes = {};
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|