@staticbolt/lsp 1.0.0-beta.30 → 1.0.0-beta.32

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.
@@ -1,244 +0,0 @@
1
- import { EventEmitter } from "node:events";
2
- import { watch, type FSWatcher } from "node:fs";
3
- import { access, constants } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
-
7
- import type { AppConfig } from "@staticbolt/core";
8
- import type { HTMLDataV1 } from "vscode-html-languageservice";
9
- import type { MarkupContent, RemoteConsole } from "vscode-languageserver";
10
-
11
- export type StaticBoltConfig = AppConfig;
12
-
13
- export interface ConfigLoaderEvents {
14
- change: [config: StaticBoltConfig];
15
- error: [err: Error];
16
- }
17
-
18
- /** Iterated in order, so the first one that exists wins. */
19
- const CONFIG_CANDIDATES = new Set([".staticbolt.ts", ".staticbolt.js"]);
20
-
21
- async function findConfigFile(workspaceRoot: string): Promise<string | null> {
22
- for (const name of CONFIG_CANDIDATES) {
23
- const full = join(workspaceRoot, name);
24
-
25
- try {
26
- await access(full, constants.R_OK);
27
- return full;
28
- } catch {
29
- // not found or not readable — try next
30
- }
31
- }
32
-
33
- return null;
34
- }
35
-
36
- async function importFresh(absolutePath: string): Promise<StaticBoltConfig> {
37
- const url = `${pathToFileURL(absolutePath).href}?t=${Date.now()}`;
38
- const module_ = (await import(url)) as { default: StaticBoltConfig };
39
-
40
- if (!("default" in module_)) {
41
- throw new Error(`Config file "${absolutePath}" has no default export. Use \`export default { … }\`.`);
42
- }
43
-
44
- return module_.default;
45
- }
46
-
47
- // eslint-disable-next-line unicorn/prefer-event-target
48
- export class ConfigLoader extends EventEmitter<ConfigLoaderEvents> {
49
- private readonly workspaceRoot: string;
50
- private configPath: string | null = null;
51
- private watcher: FSWatcher | null = null;
52
- private current: StaticBoltConfig | null = null;
53
- private debounceTimer: ReturnType<typeof setTimeout> | null = null;
54
- private readonly DEBOUNCE_MS = 150;
55
-
56
- constructor(workspaceRoot: string) {
57
- super();
58
- this.workspaceRoot = workspaceRoot;
59
- }
60
-
61
- /** Start watching. Resolves with the initial config (or null if absent). */
62
- async start(): Promise<StaticBoltConfig | null> {
63
- this.configPath = await findConfigFile(this.workspaceRoot);
64
-
65
- if (!this.configPath) {
66
- return null;
67
- }
68
-
69
- await this.load();
70
- this.watch();
71
- return this.current;
72
- }
73
-
74
- /** Stop watching and release all resources. */
75
- dispose(): void {
76
- if (this.debounceTimer !== null) {
77
- clearTimeout(this.debounceTimer);
78
- }
79
- this.watcher?.close();
80
- this.watcher = null;
81
- }
82
-
83
- /** The last successfully loaded config, or null if none loaded yet. */
84
- get config(): StaticBoltConfig | null {
85
- return this.current;
86
- }
87
-
88
- private async load(): Promise<void> {
89
- if (!this.configPath) return;
90
-
91
- try {
92
- this.current = await importFresh(this.configPath);
93
- this.emit("change", this.current);
94
- } catch (error) {
95
- this.emit("error", error instanceof Error ? error : new Error(String(error)));
96
- }
97
- }
98
-
99
- /**
100
- * The directory is watched rather than the file. A watch on the file itself is bound to the inode, so an editor saving through
101
- * a temporary file and a rename leaves it watching a replaced file, and on macOS it never reports a plain write either.
102
- */
103
- private watch(): void {
104
- this.watcher = watch(this.workspaceRoot, { persistent: false }, (_event, filename) => {
105
- if (typeof filename === "string" && CONFIG_CANDIDATES.has(filename)) {
106
- this.scheduleReload();
107
- }
108
- });
109
-
110
- this.watcher.on("error", error => this.emit("error", error));
111
- }
112
-
113
- private scheduleReload(): void {
114
- if (this.debounceTimer !== null) {
115
- clearTimeout(this.debounceTimer);
116
- }
117
-
118
- this.debounceTimer = setTimeout(async () => {
119
- this.debounceTimer = null;
120
-
121
- // Resolved again so that renaming ".staticbolt.ts" to ".staticbolt.js", or the other way round, is picked up.
122
- this.configPath = await findConfigFile(this.workspaceRoot);
123
-
124
- if (!this.configPath) {
125
- this.emit("error", new Error(`Config file was removed from: ${this.workspaceRoot}`));
126
- return;
127
- }
128
-
129
- await this.load();
130
- }, this.DEBOUNCE_MS);
131
- }
132
- }
133
-
134
- export class ConfigManager {
135
- private readonly console: RemoteConsole;
136
- readonly configs = new Map<string, ConfigLoader>();
137
- lspHtmlData: HTMLDataV1[] = [];
138
-
139
- constructor(console: RemoteConsole) {
140
- this.console = console;
141
- }
142
-
143
- async get(workspaceRoot: string): Promise<AppConfig | null> {
144
- const configLoader = this.configs.get(workspaceRoot);
145
- if (configLoader) {
146
- if (!configLoader.config) {
147
- this.configs.delete(workspaceRoot);
148
- return null;
149
- }
150
-
151
- return configLoader.config;
152
- }
153
-
154
- const newConfigLoader = new ConfigLoader(workspaceRoot);
155
- this.configs.set(workspaceRoot, newConfigLoader);
156
-
157
- newConfigLoader.on("error", error => {
158
- this.console.error(`Failed to load config file: ${error.message}`);
159
- });
160
-
161
- newConfigLoader.on("change", async config => {
162
- await this.collectData(config);
163
- });
164
-
165
- await newConfigLoader.start();
166
-
167
- if (!newConfigLoader.config) {
168
- return null;
169
- }
170
-
171
- return newConfigLoader.config;
172
- }
173
-
174
- dispose(): void {
175
- for (const configLoader of this.configs.values()) {
176
- configLoader.dispose();
177
- }
178
- }
179
-
180
- private async collectData(config: StaticBoltConfig) {
181
- // collected into a new array and published at the end, consumers cache on its identity
182
- const collected: HTMLDataV1[] = [];
183
-
184
- const plugins = config.plugins ?? [];
185
-
186
- for (const pluginOrArray of plugins) {
187
- const plugins = Array.isArray(pluginOrArray) ? pluginOrArray : [pluginOrArray];
188
-
189
- for (const plugin of plugins) {
190
- if (!plugin.lspHtmlData) {
191
- continue;
192
- }
193
-
194
- const htmlData = await plugin.lspHtmlData();
195
- if (htmlData) {
196
- htmlDataInjectPluginName(htmlData, plugin.name);
197
- collected.push(htmlData);
198
- }
199
- }
200
- }
201
-
202
- this.lspHtmlData = collected;
203
- }
204
- }
205
-
206
- function htmlDataInjectPluginName(htmlData: HTMLDataV1, pluginName: string) {
207
- const tags = htmlData.tags ?? [];
208
- for (const tag of tags) {
209
- tag.description = replaceDescription(tag.description, pluginName);
210
-
211
- const attributes = tag.attributes ?? [];
212
- for (const attribute of attributes) {
213
- attribute.description = replaceDescription(attribute.description, pluginName);
214
- }
215
- }
216
-
217
- const globalAttributes = htmlData.globalAttributes ?? [];
218
- for (const attribute of globalAttributes) {
219
- attribute.description = replaceDescription(attribute.description, pluginName);
220
- }
221
- }
222
-
223
- function replaceDescription(description: string | MarkupContent | undefined, pluginName: string) {
224
- const info = `_Provided by **staticbolt** \`${pluginName}\` plugin._`;
225
-
226
- if (!description) {
227
- return info;
228
- }
229
-
230
- if (typeof description === "string") {
231
- if (description.includes(info)) {
232
- return description;
233
- }
234
-
235
- return `${description}\n\n${info}`;
236
- }
237
-
238
- if (description.value.includes(info)) {
239
- return description;
240
- }
241
-
242
- description.value = `${description.value}\n\n${info}`;
243
- return description;
244
- }
@@ -1,23 +0,0 @@
1
- import { globSync } from "node:fs";
2
- import * as path from "node:path";
3
- import * as vscodeUri from "vscode-uri";
4
-
5
- import type { WorkspaceFolder } from "vscode-languageserver";
6
-
7
- export function findStaticboltProjects(searchRoots: WorkspaceFolder[]): WorkspaceFolder[] {
8
- const results: WorkspaceFolder[] = [];
9
-
10
- for (const root of searchRoots) {
11
- const rootPath = vscodeUri.URI.parse(root.uri).fsPath;
12
-
13
- const configs = globSync("**/.staticbolt.{ts,js}", { cwd: rootPath, exclude: ["**/node_modules/**", "**/.git/**"] });
14
-
15
- for (const configPath of configs) {
16
- const absDirectory = path.join(rootPath, path.dirname(configPath));
17
- const projectUri = vscodeUri.URI.file(absDirectory).toString();
18
- results.push({ name: path.basename(absDirectory), uri: projectUri });
19
- }
20
- }
21
-
22
- return results;
23
- }
@@ -1,191 +0,0 @@
1
- import * as vscode from "vscode-languageserver";
2
- import { TextDocument } from "vscode-languageserver-textdocument";
3
- import * as vscodeUri from "vscode-uri";
4
-
5
- import { ConfigManager } from "./helpers/config-loader.ts";
6
- import { findStaticboltProjects } from "./helpers/find-projects.ts";
7
- import { getLanguageModes, isCompletionItemData } from "./modes/language-modes.ts";
8
- import { getFileSystemProvider } from "./requests.ts";
9
- import { pushAll } from "./utils/arrays.ts";
10
- import { getDocumentContext } from "./utils/document-context.ts";
11
- import { findProjectRoot } from "./utils/find-project-root.ts";
12
- import { runSafe } from "./utils/runner.ts";
13
-
14
- import type { LanguageModes } from "./modes/language-modes.ts";
15
- import type { FileSystemProvider } from "./requests.ts";
16
- import type { Connection, Disposable, InitializeParams, WorkspaceFolder } from "vscode-languageserver";
17
-
18
- export interface RuntimeEnvironment {
19
- fileFs?: FileSystemProvider;
20
-
21
- configureHttpRequests?(proxy: string | undefined, isStrictSSL: boolean): void;
22
-
23
- readonly timer: {
24
- setImmediate(callback: (...arguments_: unknown[]) => void, ...arguments_: unknown[]): Disposable;
25
- setTimeout(callback: (...arguments_: unknown[]) => void, ms: number, ...arguments_: unknown[]): Disposable;
26
- };
27
- }
28
-
29
- export interface CustomDataRequestService {
30
- getContent(uri: string): Promise<string>;
31
- }
32
-
33
- export function startServer(connection: Connection, runtime: RuntimeEnvironment) {
34
- // Create a text document manager.
35
- const documents = new vscode.TextDocuments(TextDocument);
36
- // Make the text document manager listen on the connection
37
- // for open, change and close text document events
38
- documents.listen(connection);
39
-
40
- let lspSearchRoots: WorkspaceFolder[] = [];
41
- let workspaceFolders: WorkspaceFolder[] = [];
42
- let configManager: ConfigManager;
43
-
44
- let languageModes: LanguageModes;
45
-
46
- connection.onInitialize((parameters: InitializeParams) => {
47
- if (Array.isArray(parameters.workspaceFolders)) {
48
- lspSearchRoots = parameters.workspaceFolders;
49
- } else {
50
- lspSearchRoots = [];
51
- if (parameters.rootPath) {
52
- lspSearchRoots.push({ name: "", uri: vscodeUri.URI.file(parameters.rootPath).toString() });
53
- }
54
- }
55
-
56
- workspaceFolders = findStaticboltProjects(lspSearchRoots);
57
- connection.console.log(
58
- `[staticbolt] discovered projects:\n` + workspaceFolders.map(f => ` - [${f.name}]: ${f.uri}`).join("\n")
59
- );
60
-
61
- const fileSystemProvider = getFileSystemProvider(["file"], connection, runtime);
62
- languageModes = getLanguageModes(parameters.capabilities, fileSystemProvider);
63
-
64
- configManager = new ConfigManager(connection.console);
65
-
66
- documents.onDidClose(document => {
67
- languageModes.onDocumentRemoved(document.document);
68
- });
69
-
70
- connection.onShutdown(() => {
71
- languageModes.dispose();
72
- configManager.dispose();
73
- });
74
-
75
- return {
76
- capabilities: {
77
- textDocumentSync: vscode.TextDocumentSyncKind.Incremental,
78
- completionProvider: {
79
- resolveProvider: true,
80
- triggerCharacters: [".", ":", "<", '"', "=", "/"],
81
- },
82
- hoverProvider: true,
83
- documentLinkProvider: { resolveProvider: false },
84
- },
85
- };
86
- });
87
-
88
- connection.onInitialized(() => {});
89
-
90
- connection.onCompletion(async (textDocumentPosition, token) => {
91
- return runSafe(
92
- runtime,
93
- async () => {
94
- const projectRoot = findProjectRoot(textDocumentPosition.textDocument.uri);
95
- if (!projectRoot) return null;
96
-
97
- const document = documents.get(textDocumentPosition.textDocument.uri);
98
- if (!document) return null;
99
-
100
- const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
101
- if (!mode?.doComplete) return { isIncomplete: true, items: [] };
102
-
103
- const config = await configManager.get(projectRoot);
104
- if (!config) return { isIncomplete: true, items: [] };
105
-
106
- const htmlDocument = languageModes.getHtmlDocument(document);
107
- const documentContext = getDocumentContext(document.uri, workspaceFolders);
108
- return mode.doComplete(htmlDocument, textDocumentPosition.position, documentContext, configManager.lspHtmlData);
109
- },
110
- null,
111
- `Error while computing completions for ${textDocumentPosition.textDocument.uri}`,
112
- token
113
- );
114
- });
115
-
116
- connection.onCompletionResolve((item, token) => {
117
- return runSafe(
118
- runtime,
119
- async () => {
120
- const data = item.data as Record<string, unknown>;
121
- if (!isCompletionItemData(data)) return item;
122
-
123
- const document = documents.get(data.uri);
124
- if (!document) return item;
125
-
126
- const mode = languageModes.getMode(data.languageId);
127
- if (!mode?.doResolve) return item;
128
-
129
- return mode.doResolve(languageModes.getHtmlDocument(document), item);
130
- },
131
- item,
132
- `Error while resolving completion proposal`,
133
- token
134
- );
135
- });
136
-
137
- connection.onHover((textDocumentPosition, token) => {
138
- return runSafe(
139
- runtime,
140
- async () => {
141
- const projectRoot = findProjectRoot(textDocumentPosition.textDocument.uri);
142
- if (!projectRoot) return null;
143
-
144
- const document = documents.get(textDocumentPosition.textDocument.uri);
145
- if (!document) return null;
146
-
147
- const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
148
- if (!mode?.doHover) return null;
149
-
150
- const config = await configManager.get(projectRoot);
151
- if (!config) return null;
152
-
153
- return mode.doHover(languageModes.getHtmlDocument(document), textDocumentPosition.position, configManager.lspHtmlData);
154
- },
155
- null,
156
- `Error while computing hover for ${textDocumentPosition.textDocument.uri}`,
157
- token
158
- );
159
- });
160
-
161
- connection.onDocumentLinks((documentLinkParameter, token) => {
162
- return runSafe(
163
- runtime,
164
- async () => {
165
- const projectRoot = findProjectRoot(documentLinkParameter.textDocument.uri);
166
- if (!projectRoot) return null;
167
-
168
- const document = documents.get(documentLinkParameter.textDocument.uri);
169
- if (!document) return [];
170
-
171
- const links: vscode.DocumentLink[] = [];
172
-
173
- const htmlDocument = languageModes.getHtmlDocument(document);
174
- const documentContext = getDocumentContext(document.uri, workspaceFolders);
175
- for (const mode of languageModes.getAllModesInDocument(document)) {
176
- if (mode.findDocumentLinks) {
177
- pushAll(links, await mode.findDocumentLinks(htmlDocument, documentContext, projectRoot));
178
- }
179
- }
180
-
181
- return links;
182
- },
183
- [],
184
- `Error while document links for ${documentLinkParameter.textDocument.uri}`,
185
- token
186
- );
187
- });
188
-
189
- // Listen on the connection
190
- connection.listen();
191
- }
@@ -1,91 +0,0 @@
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;
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 (Object.hasOwn(languageModels, uri)) {
75
- delete languageModels[uri];
76
- nModels--;
77
- }
78
- },
79
-
80
- dispose() {
81
- if (cleanupInterval === undefined) {
82
- return;
83
- }
84
-
85
- clearInterval(cleanupInterval);
86
- cleanupInterval = undefined;
87
- languageModels = {};
88
- nModels = 0;
89
- },
90
- };
91
- }
@@ -1,150 +0,0 @@
1
- import vscodeHtml from "vscode-html-languageservice";
2
- import { TextDocument } from "vscode-languageserver-textdocument";
3
-
4
- import { blankRegions, findMarkdownNonHtmlRegions } from "./markdown-regions.ts";
5
-
6
- import type { LanguageService } from "vscode-html-languageservice";
7
-
8
- export interface HTMLDocumentRegions {
9
- /** The document to serve HTML features from: itself for an HTML file, a copy declared as HTML for a markdown one. */
10
- getHtmlDocument(): TextDocument;
11
- getLanguageAtPosition(position: Position): string | undefined;
12
- getLanguagesInDocument(): string[];
13
- }
14
-
15
- const TokenType = vscodeHtml.TokenType;
16
- type Position = vscodeHtml.Position;
17
-
18
- interface EmbeddedRegion {
19
- languageId: string | undefined;
20
- start: number;
21
- end: number;
22
- }
23
-
24
- export function getDocumentRegions(languageService: LanguageService, document: TextDocument): HTMLDocumentRegions {
25
- const source = document.getText();
26
- const isMarkdown = document.languageId === "markdown";
27
-
28
- const markdownRegions: EmbeddedRegion[] = isMarkdown
29
- ? findMarkdownNonHtmlRegions(source).map(region => ({ ...region, languageId: undefined }))
30
- : [];
31
-
32
- const htmlText = isMarkdown ? blankRegions(source, markdownRegions) : source;
33
-
34
- const regions: EmbeddedRegion[] = [];
35
- const scanner = languageService.createScanner(htmlText);
36
- let lastTagName: string = "";
37
- let lastAttributeName: string | null = null;
38
- let languageIdFromType: string | undefined;
39
-
40
- let token = scanner.scan();
41
-
42
- while (token !== TokenType.EOS) {
43
- switch (token) {
44
- case TokenType.StartTag: {
45
- lastTagName = scanner.getTokenText();
46
- lastAttributeName = null;
47
- languageIdFromType = "javascript";
48
- break;
49
- }
50
- case TokenType.Styles: {
51
- regions.push({ languageId: "css", start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
52
- break;
53
- }
54
- case TokenType.Script: {
55
- regions.push({ languageId: languageIdFromType, start: scanner.getTokenOffset(), end: scanner.getTokenEnd() });
56
- break;
57
- }
58
- case TokenType.AttributeName: {
59
- lastAttributeName = scanner.getTokenText();
60
- break;
61
- }
62
- case TokenType.AttributeValue: {
63
- if (lastAttributeName === "type" && lastTagName.toLowerCase() === "script") {
64
- const token = scanner.getTokenText();
65
- if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(token) || token === "module") {
66
- languageIdFromType = "javascript";
67
- } else if (/["']text\/typescript["']/.test(token)) {
68
- languageIdFromType = "typescript";
69
- } else {
70
- languageIdFromType = undefined;
71
- }
72
- } else {
73
- const attributeLanguageId = getAttributeLanguage(lastAttributeName!);
74
- if (attributeLanguageId) {
75
- let start = scanner.getTokenOffset();
76
- let end = scanner.getTokenEnd();
77
- const firstChar = htmlText[start];
78
- if (firstChar === "'" || firstChar === '"') {
79
- start++;
80
- end--;
81
- }
82
- regions.push({ languageId: attributeLanguageId, start, end });
83
- }
84
- }
85
- lastAttributeName = null;
86
- break;
87
- }
88
- }
89
- token = scanner.scan();
90
- }
91
-
92
- const allRegions = mergeRegions(regions, markdownRegions);
93
- const htmlDocument = isMarkdown ? TextDocument.create(document.uri, "html", document.version, htmlText) : document;
94
-
95
- return {
96
- getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, allRegions, position),
97
- getLanguagesInDocument: () => getLanguagesInDocument(allRegions),
98
- getHtmlDocument: () => htmlDocument,
99
- };
100
- }
101
-
102
- /** The lookups below read the regions in order, so an overlap is dropped: a scanned region always wins over a markdown one. */
103
- function mergeRegions(scanned: EmbeddedRegion[], markdown: EmbeddedRegion[]): EmbeddedRegion[] {
104
- if (markdown.length === 0) return scanned;
105
-
106
- const merged = [...scanned, ...markdown].toSorted((a, b) => a.start - b.start || b.end - a.end);
107
- const result: EmbeddedRegion[] = [];
108
-
109
- for (const region of merged) {
110
- const previous = result.at(-1);
111
- if (previous && region.start < previous.end) continue;
112
- result.push(region);
113
- }
114
-
115
- return result;
116
- }
117
-
118
- function getLanguagesInDocument(regions: EmbeddedRegion[]): string[] {
119
- const languages = new Set(["html"]);
120
-
121
- for (const region of regions) {
122
- if (region.languageId) {
123
- languages.add(region.languageId);
124
- }
125
- }
126
-
127
- return [...languages];
128
- }
129
-
130
- function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined {
131
- const offset = document.offsetAt(position);
132
- for (const region of regions) {
133
- if (region.start <= offset) {
134
- if (offset <= region.end) {
135
- return region.languageId;
136
- }
137
- } else {
138
- break;
139
- }
140
- }
141
- return "html";
142
- }
143
-
144
- function getAttributeLanguage(attributeName: string): string | null {
145
- const match = attributeName.match(/^(style)$|^(on\w+)$/i);
146
- if (!match) {
147
- return null;
148
- }
149
- return match[1] ? "css" : "javascript";
150
- }