@staticbolt/lsp 1.0.0-beta.25 → 1.0.0-beta.27
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/lib/index.mjs +329 -184
- package/lib/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/helpers/config-loader.ts +19 -14
- package/src/helpers/merge-html-data.ts +253 -0
- package/src/html-server.ts +8 -17
- package/src/index.ts +14 -2
- package/src/modes/embedded-support.ts +40 -172
- package/src/modes/html-mode.ts +17 -33
- package/src/modes/language-modes.ts +8 -69
- package/src/modes/markdown-regions.ts +92 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HTMLDataV1,
|
|
3
|
+
IAttributeData,
|
|
4
|
+
IHTMLDataProvider,
|
|
5
|
+
IReference,
|
|
6
|
+
ITagData,
|
|
7
|
+
IValueData,
|
|
8
|
+
MarkupContent,
|
|
9
|
+
} from "vscode-html-languageservice";
|
|
10
|
+
|
|
11
|
+
type Description = string | MarkupContent | undefined;
|
|
12
|
+
type DescriptionKind = MarkupContent["kind"];
|
|
13
|
+
type ValueSets = Map<string, IValueData[]>;
|
|
14
|
+
|
|
15
|
+
/** Separates the documentation of two contributors. Rendered as a horizontal rule, unless the description is plain text. */
|
|
16
|
+
const MARKDOWN_SEPARATOR = "\n\n---\n\n";
|
|
17
|
+
const PLAINTEXT_SEPARATOR = "\n\n";
|
|
18
|
+
|
|
19
|
+
/** Groups entries by key, preserving the order of first appearance. */
|
|
20
|
+
function groupBy<T>(items: T[], toKey: (item: T) => string): T[][] {
|
|
21
|
+
const groups = new Map<string, T[]>();
|
|
22
|
+
const ordered: T[][] = [];
|
|
23
|
+
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
const key = toKey(item);
|
|
26
|
+
|
|
27
|
+
let group = groups.get(key);
|
|
28
|
+
if (!group) {
|
|
29
|
+
group = [];
|
|
30
|
+
groups.set(key, group);
|
|
31
|
+
ordered.push(group);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
group.push(item);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return ordered;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Markdown wins over plain text, so a contributor asking for rich text still gets it. Plain strings imply no preference. */
|
|
41
|
+
function mergeDescriptionKind(descriptions: Exclude<Description, undefined>[]): DescriptionKind | undefined {
|
|
42
|
+
let kind: DescriptionKind | undefined;
|
|
43
|
+
|
|
44
|
+
for (const description of descriptions) {
|
|
45
|
+
if (typeof description === "string" || kind === "markdown") continue;
|
|
46
|
+
kind = description.kind;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return kind;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Concatenates every distinct description, so no contributor's documentation gets lost. */
|
|
53
|
+
function mergeDescriptions(descriptions: Description[]): Description {
|
|
54
|
+
const defined = descriptions.filter(description => description !== undefined);
|
|
55
|
+
|
|
56
|
+
// nothing to merge, keep the original as-is
|
|
57
|
+
if (defined.length <= 1) return defined[0];
|
|
58
|
+
|
|
59
|
+
const parts: string[] = [];
|
|
60
|
+
const seen = new Set<string>();
|
|
61
|
+
|
|
62
|
+
for (const description of defined) {
|
|
63
|
+
const text = (typeof description === "string" ? description : description.value).trim();
|
|
64
|
+
if (!text || seen.has(text)) continue;
|
|
65
|
+
|
|
66
|
+
seen.add(text);
|
|
67
|
+
parts.push(text);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (parts.length === 0) return undefined;
|
|
71
|
+
|
|
72
|
+
const kind = mergeDescriptionKind(defined);
|
|
73
|
+
const value = parts.join(kind === "plaintext" ? PLAINTEXT_SEPARATOR : MARKDOWN_SEPARATOR);
|
|
74
|
+
|
|
75
|
+
return kind ? { kind, value } : value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mergeReferences(references: (IReference[] | undefined)[]): IReference[] | undefined {
|
|
79
|
+
const merged: IReference[] = [];
|
|
80
|
+
const seen = new Set<string>();
|
|
81
|
+
|
|
82
|
+
for (const list of references) {
|
|
83
|
+
const entries = list ?? [];
|
|
84
|
+
|
|
85
|
+
for (const reference of entries) {
|
|
86
|
+
const key = `${reference.name} ${reference.url}`;
|
|
87
|
+
if (seen.has(key)) continue;
|
|
88
|
+
|
|
89
|
+
seen.add(key);
|
|
90
|
+
merged.push(reference);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return merged.length > 0 ? merged : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mergeBrowsers(browsers: (string[] | undefined)[]): string[] | undefined {
|
|
98
|
+
const merged = new Set<string>();
|
|
99
|
+
|
|
100
|
+
for (const list of browsers) {
|
|
101
|
+
const entries = list ?? [];
|
|
102
|
+
|
|
103
|
+
for (const browser of entries) {
|
|
104
|
+
merged.add(browser);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return merged.size > 0 ? [...merged] : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The values an attribute contributes, with its `valueSet` reference expanded. */
|
|
112
|
+
function resolveValues(attribute: IAttributeData, valueSets: ValueSets): IValueData[] {
|
|
113
|
+
const values = attribute.values ?? [];
|
|
114
|
+
if (!attribute.valueSet) return values;
|
|
115
|
+
|
|
116
|
+
return [...(valueSets.get(attribute.valueSet) ?? []), ...values];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Merges entries sharing the same value name into one. Unlike tags and attributes, value names are case-sensitive. */
|
|
120
|
+
function mergeValues(values: IValueData[]): IValueData[] {
|
|
121
|
+
return groupBy(values, value => value.name).map(group => {
|
|
122
|
+
if (group.length === 1) return group[0];
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: group[0].name,
|
|
126
|
+
description: mergeDescriptions(group.map(value => value.description)),
|
|
127
|
+
references: mergeReferences(group.map(value => value.references)),
|
|
128
|
+
browsers: mergeBrowsers(group.map(value => value.browsers)),
|
|
129
|
+
status: group.find(value => value.status)?.status,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Merges entries sharing the same attribute name into one, combining their descriptions, values, references and browser support.
|
|
136
|
+
*
|
|
137
|
+
* `valueSet` references are expanded into the merged `values`, so attributes contributed by different plugins can each bring
|
|
138
|
+
* their own value set and still end up with a single, complete value list.
|
|
139
|
+
*/
|
|
140
|
+
function mergeAttributes(attributes: IAttributeData[], valueSets: ValueSets): IAttributeData[] {
|
|
141
|
+
return groupBy(attributes, attribute => attribute.name.toLowerCase()).map(group => {
|
|
142
|
+
// nothing to merge and no value set to expand, keep the original as-is
|
|
143
|
+
if (group.length === 1 && !group[0].valueSet) return group[0];
|
|
144
|
+
|
|
145
|
+
const values = mergeValues(group.flatMap(attribute => resolveValues(attribute, valueSets)));
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
name: group[0].name,
|
|
149
|
+
description: mergeDescriptions(group.map(attribute => attribute.description)),
|
|
150
|
+
// `valueSet` is dropped on purpose: every referenced set is now expanded into `values`
|
|
151
|
+
values: values.length > 0 ? values : undefined,
|
|
152
|
+
references: mergeReferences(group.map(attribute => attribute.references)),
|
|
153
|
+
browsers: mergeBrowsers(group.map(attribute => attribute.browsers)),
|
|
154
|
+
status: group.find(attribute => attribute.status)?.status,
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Merges entries sharing the same tag name into one, including their attributes. */
|
|
160
|
+
function mergeTags(tags: ITagData[], valueSets: ValueSets): ITagData[] {
|
|
161
|
+
return groupBy(tags, tag => tag.name.toLowerCase()).map(group => {
|
|
162
|
+
const attributes = mergeAttributes(
|
|
163
|
+
group.flatMap(tag => tag.attributes ?? []),
|
|
164
|
+
valueSets
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (group.length === 1) return { ...group[0], attributes };
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
name: group[0].name,
|
|
171
|
+
description: mergeDescriptions(group.map(tag => tag.description)),
|
|
172
|
+
attributes,
|
|
173
|
+
references: mergeReferences(group.map(tag => tag.references)),
|
|
174
|
+
browsers: mergeBrowsers(group.map(tag => tag.browsers)),
|
|
175
|
+
status: group.find(tag => tag.status)?.status,
|
|
176
|
+
void: group.some(tag => tag.void),
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Value sets sharing a name are merged, so an attribute referencing one gets the values of every contributor. */
|
|
182
|
+
function collectValueSets(htmlData: HTMLDataV1[]): ValueSets {
|
|
183
|
+
const valueSets: ValueSets = new Map();
|
|
184
|
+
const collected = htmlData.flatMap(data => data.valueSets ?? []);
|
|
185
|
+
|
|
186
|
+
for (const valueSet of collected) {
|
|
187
|
+
const existing = valueSets.get(valueSet.name);
|
|
188
|
+
valueSets.set(valueSet.name, existing ? mergeValues([...existing, ...valueSet.values]) : valueSet.values);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return valueSets;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Builds a single data provider out of every collected `HTMLDataV1`, merging tags, attributes and values that share the same name
|
|
196
|
+
* instead of reporting them once per contributor.
|
|
197
|
+
*/
|
|
198
|
+
export function createMergedHtmlDataProvider(id: string, htmlData: HTMLDataV1[]): IHTMLDataProvider {
|
|
199
|
+
const valueSets = collectValueSets(htmlData);
|
|
200
|
+
|
|
201
|
+
// the language service asks for tags on every parse, and for attributes on every completion/hover, so merging happens once here
|
|
202
|
+
const tags = mergeTags(
|
|
203
|
+
htmlData.flatMap(data => data.tags ?? []),
|
|
204
|
+
valueSets
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const globalAttributes = mergeAttributes(
|
|
208
|
+
htmlData.flatMap(data => data.globalAttributes ?? []),
|
|
209
|
+
valueSets
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const tagsByName = new Map(tags.map(tag => [tag.name.toLowerCase(), tag]));
|
|
213
|
+
const attributesByTag = new Map<string, IAttributeData[]>();
|
|
214
|
+
|
|
215
|
+
function provideAttributes(tag: string): IAttributeData[] {
|
|
216
|
+
const key = tag.toLowerCase();
|
|
217
|
+
|
|
218
|
+
const cached = attributesByTag.get(key);
|
|
219
|
+
if (cached) return cached;
|
|
220
|
+
|
|
221
|
+
// an unknown tag only sees the global attributes, which are merged already
|
|
222
|
+
const tagAttributes = tagsByName.get(key)?.attributes;
|
|
223
|
+
if (!tagAttributes || tagAttributes.length === 0) return globalAttributes;
|
|
224
|
+
|
|
225
|
+
const attributes = mergeAttributes([...globalAttributes, ...tagAttributes], valueSets);
|
|
226
|
+
attributesByTag.set(key, attributes);
|
|
227
|
+
|
|
228
|
+
return attributes;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
getId() {
|
|
233
|
+
return id;
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
isApplicable(languageId) {
|
|
237
|
+
return languageId === "html";
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
provideTags() {
|
|
241
|
+
return tags;
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
provideAttributes,
|
|
245
|
+
|
|
246
|
+
provideValues(tag, attribute) {
|
|
247
|
+
const name = attribute.toLowerCase();
|
|
248
|
+
|
|
249
|
+
// merged attributes already carry their expanded value sets
|
|
250
|
+
return provideAttributes(tag).find(a => a.name.toLowerCase() === name)?.values ?? [];
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
package/src/html-server.ts
CHANGED
|
@@ -4,14 +4,14 @@ import * as vscodeUri from "vscode-uri";
|
|
|
4
4
|
|
|
5
5
|
import { ConfigManager } from "./helpers/config-loader.ts";
|
|
6
6
|
import { findStaticboltProjects } from "./helpers/find-projects.ts";
|
|
7
|
-
import { getLanguageModes } from "./modes/language-modes.ts";
|
|
7
|
+
import { getLanguageModes, isCompletionItemData } from "./modes/language-modes.ts";
|
|
8
8
|
import { getFileSystemProvider } from "./requests.ts";
|
|
9
9
|
import { pushAll } from "./utils/arrays.ts";
|
|
10
10
|
import { getDocumentContext } from "./utils/document-context.ts";
|
|
11
11
|
import { findProjectRoot } from "./utils/find-project-root.ts";
|
|
12
12
|
import { runSafe } from "./utils/runner.ts";
|
|
13
13
|
|
|
14
|
-
import type {
|
|
14
|
+
import type { LanguageModes } from "./modes/language-modes.ts";
|
|
15
15
|
import type { FileSystemProvider } from "./requests.ts";
|
|
16
16
|
import type { Connection, Disposable, InitializeParams, WorkspaceFolder } from "vscode-languageserver";
|
|
17
17
|
|
|
@@ -43,13 +43,6 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
|
|
|
43
43
|
|
|
44
44
|
let languageModes: LanguageModes;
|
|
45
45
|
|
|
46
|
-
const documentSettings: { [key: string]: Thenable<Settings> } = {};
|
|
47
|
-
|
|
48
|
-
// remove document settings on close
|
|
49
|
-
documents.onDidClose(document => {
|
|
50
|
-
delete documentSettings[document.document.uri];
|
|
51
|
-
});
|
|
52
|
-
|
|
53
46
|
connection.onInitialize((parameters: InitializeParams) => {
|
|
54
47
|
if (Array.isArray(parameters.workspaceFolders)) {
|
|
55
48
|
lspSearchRoots = parameters.workspaceFolders;
|
|
@@ -110,8 +103,9 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
|
|
|
110
103
|
const config = await configManager.get(projectRoot);
|
|
111
104
|
if (!config) return { isIncomplete: true, items: [] };
|
|
112
105
|
|
|
106
|
+
const htmlDocument = languageModes.getHtmlDocument(document);
|
|
113
107
|
const documentContext = getDocumentContext(document.uri, workspaceFolders);
|
|
114
|
-
return mode.doComplete(
|
|
108
|
+
return mode.doComplete(htmlDocument, textDocumentPosition.position, documentContext, configManager.lspHtmlData);
|
|
115
109
|
},
|
|
116
110
|
null,
|
|
117
111
|
`Error while computing completions for ${textDocumentPosition.textDocument.uri}`,
|
|
@@ -132,7 +126,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
|
|
|
132
126
|
const mode = languageModes.getMode(data.languageId);
|
|
133
127
|
if (!mode?.doResolve) return item;
|
|
134
128
|
|
|
135
|
-
return mode.doResolve(document, item);
|
|
129
|
+
return mode.doResolve(languageModes.getHtmlDocument(document), item);
|
|
136
130
|
},
|
|
137
131
|
item,
|
|
138
132
|
`Error while resolving completion proposal`,
|
|
@@ -156,7 +150,7 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
|
|
|
156
150
|
const config = await configManager.get(projectRoot);
|
|
157
151
|
if (!config) return null;
|
|
158
152
|
|
|
159
|
-
return mode.doHover(document, textDocumentPosition.position, configManager.lspHtmlData);
|
|
153
|
+
return mode.doHover(languageModes.getHtmlDocument(document), textDocumentPosition.position, configManager.lspHtmlData);
|
|
160
154
|
},
|
|
161
155
|
null,
|
|
162
156
|
`Error while computing hover for ${textDocumentPosition.textDocument.uri}`,
|
|
@@ -176,10 +170,11 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
|
|
|
176
170
|
|
|
177
171
|
const links: vscode.DocumentLink[] = [];
|
|
178
172
|
|
|
173
|
+
const htmlDocument = languageModes.getHtmlDocument(document);
|
|
179
174
|
const documentContext = getDocumentContext(document.uri, workspaceFolders);
|
|
180
175
|
for (const mode of languageModes.getAllModesInDocument(document)) {
|
|
181
176
|
if (mode.findDocumentLinks) {
|
|
182
|
-
pushAll(links, await mode.findDocumentLinks(
|
|
177
|
+
pushAll(links, await mode.findDocumentLinks(htmlDocument, documentContext, projectRoot));
|
|
183
178
|
}
|
|
184
179
|
}
|
|
185
180
|
|
|
@@ -194,7 +189,3 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment)
|
|
|
194
189
|
// Listen on the connection
|
|
195
190
|
connection.listen();
|
|
196
191
|
}
|
|
197
|
-
|
|
198
|
-
export function isCompletionItemData(value: Record<string, unknown>): value is CompletionItemData {
|
|
199
|
-
return value && typeof value.languageId === "string" && typeof value.uri === "string" && typeof value.offset === "number";
|
|
200
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { format, stripVTControlCharacters } from "node:util";
|
|
1
2
|
import * as vs from "vscode-languageserver/node";
|
|
2
3
|
|
|
3
4
|
import { startServer } from "./html-server.ts";
|
|
@@ -10,8 +11,19 @@ import type { Connection, Disposable } from "vscode-languageserver/node";
|
|
|
10
11
|
// Create a connection for the server.
|
|
11
12
|
const connection: Connection = vs.createConnection();
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
console
|
|
14
|
+
/**
|
|
15
|
+
* `RemoteConsole` takes a single string, but the shared logger calls `console` with several arguments and colours them with
|
|
16
|
+
* chalk. Passing the methods straight through drops everything after the first argument, and the output panel renders no ANSI —
|
|
17
|
+
* so format the arguments the way `console` would, then strip the escapes.
|
|
18
|
+
*/
|
|
19
|
+
function forward(write: (message: string) => void) {
|
|
20
|
+
return (...messages: unknown[]) => write(stripVTControlCharacters(format(...messages)));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log = forward(connection.console.log.bind(connection.console));
|
|
24
|
+
console.info = forward(connection.console.info.bind(connection.console));
|
|
25
|
+
console.warn = forward(connection.console.warn.bind(connection.console));
|
|
26
|
+
console.error = forward(connection.console.error.bind(connection.console));
|
|
15
27
|
|
|
16
28
|
process.on("unhandledRejection", (error: unknown) => {
|
|
17
29
|
connection.console.error(formatError(`Unhandled exception`, error));
|
|
@@ -1,40 +1,41 @@
|
|
|
1
1
|
import vscodeHtml from "vscode-html-languageservice";
|
|
2
2
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import { blankRegions, findMarkdownNonHtmlRegions } from "./markdown-regions.ts";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
languageId: string | undefined;
|
|
8
|
-
attributeValue?: boolean;
|
|
9
|
-
}
|
|
6
|
+
import type { LanguageService } from "vscode-html-languageservice";
|
|
10
7
|
|
|
11
8
|
export interface HTMLDocumentRegions {
|
|
12
|
-
|
|
13
|
-
|
|
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;
|
|
14
11
|
getLanguageAtPosition(position: Position): string | undefined;
|
|
15
12
|
getLanguagesInDocument(): string[];
|
|
16
|
-
getImportedScripts(): string[];
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
const CSS_STYLE_RULE = "__";
|
|
20
15
|
const TokenType = vscodeHtml.TokenType;
|
|
21
|
-
const Position = vscodeHtml.Position;
|
|
22
16
|
type Position = vscodeHtml.Position;
|
|
23
17
|
|
|
24
18
|
interface EmbeddedRegion {
|
|
25
19
|
languageId: string | undefined;
|
|
26
20
|
start: number;
|
|
27
21
|
end: number;
|
|
28
|
-
attributeValue?: boolean;
|
|
29
22
|
}
|
|
30
23
|
|
|
31
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
|
+
|
|
32
34
|
const regions: EmbeddedRegion[] = [];
|
|
33
|
-
const scanner = languageService.createScanner(
|
|
35
|
+
const scanner = languageService.createScanner(htmlText);
|
|
34
36
|
let lastTagName: string = "";
|
|
35
37
|
let lastAttributeName: string | null = null;
|
|
36
38
|
let languageIdFromType: string | undefined;
|
|
37
|
-
const importedScripts: string[] = [];
|
|
38
39
|
|
|
39
40
|
let token = scanner.scan();
|
|
40
41
|
|
|
@@ -59,13 +60,7 @@ export function getDocumentRegions(languageService: LanguageService, document: T
|
|
|
59
60
|
break;
|
|
60
61
|
}
|
|
61
62
|
case TokenType.AttributeValue: {
|
|
62
|
-
if (lastAttributeName === "
|
|
63
|
-
let value = scanner.getTokenText();
|
|
64
|
-
if (value[0] === "'" || value[0] === '"') {
|
|
65
|
-
value = value.slice(1, -1);
|
|
66
|
-
}
|
|
67
|
-
importedScripts.push(value);
|
|
68
|
-
} else if (lastAttributeName === "type" && lastTagName.toLowerCase() === "script") {
|
|
63
|
+
if (lastAttributeName === "type" && lastTagName.toLowerCase() === "script") {
|
|
69
64
|
const token = scanner.getTokenText();
|
|
70
65
|
if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(token) || token === "module") {
|
|
71
66
|
languageIdFromType = "javascript";
|
|
@@ -79,12 +74,12 @@ export function getDocumentRegions(languageService: LanguageService, document: T
|
|
|
79
74
|
if (attributeLanguageId) {
|
|
80
75
|
let start = scanner.getTokenOffset();
|
|
81
76
|
let end = scanner.getTokenEnd();
|
|
82
|
-
const firstChar =
|
|
77
|
+
const firstChar = htmlText[start];
|
|
83
78
|
if (firstChar === "'" || firstChar === '"') {
|
|
84
79
|
start++;
|
|
85
80
|
end--;
|
|
86
81
|
}
|
|
87
|
-
regions.push({ languageId: attributeLanguageId, start, end
|
|
82
|
+
regions.push({ languageId: attributeLanguageId, start, end });
|
|
88
83
|
}
|
|
89
84
|
}
|
|
90
85
|
lastAttributeName = null;
|
|
@@ -93,76 +88,43 @@ export function getDocumentRegions(languageService: LanguageService, document: T
|
|
|
93
88
|
}
|
|
94
89
|
token = scanner.scan();
|
|
95
90
|
}
|
|
91
|
+
|
|
92
|
+
const allRegions = mergeRegions(regions, markdownRegions);
|
|
93
|
+
const htmlDocument = isMarkdown ? TextDocument.create(document.uri, "html", document.version, htmlText) : document;
|
|
94
|
+
|
|
96
95
|
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, regions, position),
|
|
101
|
-
getLanguagesInDocument: () => getLanguagesInDocument(document, regions),
|
|
102
|
-
getImportedScripts: () => importedScripts,
|
|
96
|
+
getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, allRegions, position),
|
|
97
|
+
getLanguagesInDocument: () => getLanguagesInDocument(allRegions),
|
|
98
|
+
getHtmlDocument: () => htmlDocument,
|
|
103
99
|
};
|
|
104
100
|
}
|
|
105
101
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
let currentOffset = range ? document.offsetAt(range.start) : 0;
|
|
110
|
-
const endOffset = range ? document.offsetAt(range.end) : document.getText().length;
|
|
111
|
-
for (const region of regions) {
|
|
112
|
-
if (!(region.end > currentOffset && region.start < endOffset)) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
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;
|
|
115
105
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
const end = Math.min(region.end, endOffset);
|
|
126
|
-
const endPos = document.positionAt(end);
|
|
127
|
-
if (end > region.start) {
|
|
128
|
-
result.push({
|
|
129
|
-
start: startPos,
|
|
130
|
-
end: endPos,
|
|
131
|
-
languageId: region.languageId,
|
|
132
|
-
attributeValue: region.attributeValue,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
currentOffset = end;
|
|
136
|
-
currentPos = endPos;
|
|
137
|
-
}
|
|
138
|
-
if (currentOffset < endOffset) {
|
|
139
|
-
const endPos = range ? range.end : document.positionAt(endOffset);
|
|
140
|
-
result.push({
|
|
141
|
-
start: currentPos,
|
|
142
|
-
end: endPos,
|
|
143
|
-
languageId: "html",
|
|
144
|
-
});
|
|
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);
|
|
145
113
|
}
|
|
114
|
+
|
|
146
115
|
return result;
|
|
147
116
|
}
|
|
148
117
|
|
|
149
|
-
function getLanguagesInDocument(
|
|
150
|
-
const
|
|
118
|
+
function getLanguagesInDocument(regions: EmbeddedRegion[]): string[] {
|
|
119
|
+
const languages = new Set(["html"]);
|
|
151
120
|
|
|
152
121
|
for (const region of regions) {
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
result.push(region.languageId);
|
|
158
|
-
if (result.length === 3) {
|
|
159
|
-
return result;
|
|
122
|
+
if (region.languageId) {
|
|
123
|
+
languages.add(region.languageId);
|
|
160
124
|
}
|
|
161
125
|
}
|
|
162
126
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return result;
|
|
127
|
+
return [...languages];
|
|
166
128
|
}
|
|
167
129
|
|
|
168
130
|
function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined {
|
|
@@ -179,100 +141,6 @@ function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[]
|
|
|
179
141
|
return "html";
|
|
180
142
|
}
|
|
181
143
|
|
|
182
|
-
function getEmbeddedDocument(
|
|
183
|
-
document: TextDocument,
|
|
184
|
-
contents: EmbeddedRegion[],
|
|
185
|
-
languageId: string,
|
|
186
|
-
shouldIgnoreAttributeValues: boolean
|
|
187
|
-
): TextDocument {
|
|
188
|
-
let currentPos = 0;
|
|
189
|
-
const oldContent = document.getText();
|
|
190
|
-
let result = "";
|
|
191
|
-
let lastSuffix = "";
|
|
192
|
-
for (const c of contents) {
|
|
193
|
-
if (!(c.languageId === languageId && (!shouldIgnoreAttributeValues || !c.attributeValue))) {
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c));
|
|
198
|
-
result += updateContent(c, oldContent.slice(c.start, c.end));
|
|
199
|
-
currentPos = c.end;
|
|
200
|
-
lastSuffix = getSuffix(c);
|
|
201
|
-
}
|
|
202
|
-
result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, "");
|
|
203
|
-
return TextDocument.create(document.uri, languageId, document.version, result);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function getPrefix(c: EmbeddedRegion) {
|
|
207
|
-
if (c.attributeValue) {
|
|
208
|
-
switch (c.languageId) {
|
|
209
|
-
case "css": {
|
|
210
|
-
return CSS_STYLE_RULE + "{";
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return "";
|
|
215
|
-
}
|
|
216
|
-
function getSuffix(c: EmbeddedRegion) {
|
|
217
|
-
if (c.attributeValue) {
|
|
218
|
-
switch (c.languageId) {
|
|
219
|
-
case "css": {
|
|
220
|
-
return "}";
|
|
221
|
-
}
|
|
222
|
-
case "javascript": {
|
|
223
|
-
return ";";
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return "";
|
|
228
|
-
}
|
|
229
|
-
function updateContent(c: EmbeddedRegion, content: string): string {
|
|
230
|
-
if (!c.attributeValue && c.languageId === "javascript") {
|
|
231
|
-
return content.replace(`<!--`, `/* `).replace(`-->`, ` */`);
|
|
232
|
-
}
|
|
233
|
-
if (c.languageId === "css") {
|
|
234
|
-
const quoteEscape = /("|")/g;
|
|
235
|
-
return content.replace(quoteEscape, (match, _, offset: number) => {
|
|
236
|
-
const spaces = " ".repeat(match.length - 1);
|
|
237
|
-
const afterChar = content[offset + match.length];
|
|
238
|
-
if (!afterChar || afterChar.includes(" ")) {
|
|
239
|
-
return `${spaces}"`;
|
|
240
|
-
}
|
|
241
|
-
return `"${spaces}`;
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
return content;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) {
|
|
248
|
-
result += before;
|
|
249
|
-
let accumulatedWS = -before.length; // start with a negative value to account for the before string
|
|
250
|
-
for (let index = start; index < end; index++) {
|
|
251
|
-
const ch = oldContent[index];
|
|
252
|
-
if (ch === "\n" || ch === "\r") {
|
|
253
|
-
// only write new lines, skip the whitespace
|
|
254
|
-
accumulatedWS = 0;
|
|
255
|
-
result += ch;
|
|
256
|
-
} else {
|
|
257
|
-
accumulatedWS++;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
result = append(result, " ", accumulatedWS - after.length);
|
|
261
|
-
result += after;
|
|
262
|
-
return result;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function append(result: string, string_: string, n: number): string {
|
|
266
|
-
while (n > 0) {
|
|
267
|
-
if (n & 1) {
|
|
268
|
-
result += string_;
|
|
269
|
-
}
|
|
270
|
-
n >>= 1;
|
|
271
|
-
string_ += string_;
|
|
272
|
-
}
|
|
273
|
-
return result;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
144
|
function getAttributeLanguage(attributeName: string): string | null {
|
|
277
145
|
const match = attributeName.match(/^(style)$|^(on\w+)$/i);
|
|
278
146
|
if (!match) {
|