@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
package/src/modes/html-mode.ts
CHANGED
|
@@ -3,22 +3,36 @@ import path from "node:path";
|
|
|
3
3
|
import { Resolver } from "@staticbolt/core";
|
|
4
4
|
import * as vscodeUri from "vscode-uri";
|
|
5
5
|
|
|
6
|
+
import { createMergedHtmlDataProvider } from "../helpers/merge-html-data.ts";
|
|
6
7
|
import { getLanguageModelCache } from "../language-model-cache.ts";
|
|
7
8
|
import { FILE_PROTOCOL } from "./language-modes.ts";
|
|
8
9
|
|
|
9
10
|
import type { LanguageMode } from "./language-modes.ts";
|
|
10
|
-
import type { HTMLDataV1, HTMLDocument, LanguageService, TextDocument } from "vscode-html-languageservice";
|
|
11
|
+
import type { HTMLDataV1, HTMLDocument, IHTMLDataProvider, LanguageService, TextDocument } from "vscode-html-languageservice";
|
|
11
12
|
|
|
12
13
|
export function getHTMLMode(htmlLanguageService: LanguageService): LanguageMode {
|
|
13
14
|
const htmlDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
|
|
14
15
|
|
|
16
|
+
// the merged provider is rebuilt only when the collected html data actually changes
|
|
17
|
+
let lastHtmlData: HTMLDataV1[] | undefined;
|
|
18
|
+
let lastProvider: IHTMLDataProvider | undefined;
|
|
19
|
+
|
|
20
|
+
function setHtmlDataProviders(htmlData: HTMLDataV1[]) {
|
|
21
|
+
if (!lastProvider || lastHtmlData !== htmlData) {
|
|
22
|
+
lastHtmlData = htmlData;
|
|
23
|
+
lastProvider = createMergedHtmlDataProvider(FILE_PROTOCOL, htmlData);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
htmlLanguageService.setDataProviders(false, [lastProvider]);
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
return {
|
|
16
30
|
getId() {
|
|
17
31
|
return "html";
|
|
18
32
|
},
|
|
19
33
|
|
|
20
34
|
async doComplete(document, position, documentContext, htmlData) {
|
|
21
|
-
setHtmlDataProviders(
|
|
35
|
+
setHtmlDataProviders(htmlData);
|
|
22
36
|
|
|
23
37
|
const htmlDocument = htmlDocuments.get(document);
|
|
24
38
|
const completionList = await htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext);
|
|
@@ -31,7 +45,7 @@ export function getHTMLMode(htmlLanguageService: LanguageService): LanguageMode
|
|
|
31
45
|
},
|
|
32
46
|
|
|
33
47
|
async doHover(document, position, htmlData) {
|
|
34
|
-
setHtmlDataProviders(
|
|
48
|
+
setHtmlDataProviders(htmlData);
|
|
35
49
|
|
|
36
50
|
return htmlLanguageService.doHover(document, position, htmlDocuments.get(document));
|
|
37
51
|
},
|
|
@@ -67,33 +81,3 @@ export function getHTMLMode(htmlLanguageService: LanguageService): LanguageMode
|
|
|
67
81
|
},
|
|
68
82
|
};
|
|
69
83
|
}
|
|
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
|
-
}
|
|
@@ -7,44 +7,10 @@ import { getHTMLMode } from "./html-mode.ts";
|
|
|
7
7
|
import type { LanguageModelCache } from "../language-model-cache.ts";
|
|
8
8
|
import type { FileSystemProvider } from "../requests.ts";
|
|
9
9
|
import type { HTMLDocumentRegions } from "./embedded-support.ts";
|
|
10
|
-
import type {
|
|
11
|
-
|
|
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";
|
|
10
|
+
import type { ClientCapabilities, DocumentContext, HTMLDataV1, IHTMLDataProvider } from "vscode-html-languageservice";
|
|
11
|
+
import type { CompletionItem, CompletionList, DocumentLink, Hover, Position } from "vscode-languageserver";
|
|
27
12
|
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
28
13
|
|
|
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
14
|
export type CompletionItemData = {
|
|
49
15
|
languageId: string;
|
|
50
16
|
uri: string;
|
|
@@ -72,20 +38,15 @@ export interface LanguageMode {
|
|
|
72
38
|
|
|
73
39
|
export interface LanguageModes {
|
|
74
40
|
updateDataProviders(dataProviders: IHTMLDataProvider[]): void;
|
|
41
|
+
/** The document to hand to the HTML mode: itself for an HTML file, a copy holding only the raw HTML for a markdown one. */
|
|
42
|
+
getHtmlDocument(document: TextDocument): TextDocument;
|
|
75
43
|
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined;
|
|
76
|
-
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[];
|
|
77
|
-
getAllModes(): LanguageMode[];
|
|
78
44
|
getAllModesInDocument(document: TextDocument): LanguageMode[];
|
|
79
45
|
getMode(languageId: string): LanguageMode | undefined;
|
|
80
46
|
onDocumentRemoved(document: TextDocument): void;
|
|
81
47
|
dispose(): void;
|
|
82
48
|
}
|
|
83
49
|
|
|
84
|
-
export interface LanguageModeRange extends Range {
|
|
85
|
-
mode: LanguageMode | undefined;
|
|
86
|
-
attributeValue?: boolean;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
50
|
export const FILE_PROTOCOL = "staticbolt-server";
|
|
90
51
|
|
|
91
52
|
export function getLanguageModes(clientCapabilities: ClientCapabilities, requestService: FileSystemProvider): LanguageModes {
|
|
@@ -140,6 +101,10 @@ export function getLanguageModes(clientCapabilities: ClientCapabilities, request
|
|
|
140
101
|
htmlLanguageService.setDataProviders(true, dataProviders);
|
|
141
102
|
},
|
|
142
103
|
|
|
104
|
+
getHtmlDocument(document: TextDocument): TextDocument {
|
|
105
|
+
return documentRegions.get(document).getHtmlDocument();
|
|
106
|
+
},
|
|
107
|
+
|
|
143
108
|
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined {
|
|
144
109
|
const languageId = documentRegions.get(document).getLanguageAtPosition(position);
|
|
145
110
|
if (languageId) {
|
|
@@ -149,20 +114,6 @@ export function getLanguageModes(clientCapabilities: ClientCapabilities, request
|
|
|
149
114
|
return undefined;
|
|
150
115
|
},
|
|
151
116
|
|
|
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
117
|
getAllModesInDocument(document: TextDocument): LanguageMode[] {
|
|
167
118
|
const result: LanguageMode[] = [];
|
|
168
119
|
for (const languageId of documentRegions.get(document).getLanguagesInDocument()) {
|
|
@@ -175,18 +126,6 @@ export function getLanguageModes(clientCapabilities: ClientCapabilities, request
|
|
|
175
126
|
return result;
|
|
176
127
|
},
|
|
177
128
|
|
|
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
129
|
getMode(languageId: string): LanguageMode {
|
|
191
130
|
return modes[languageId];
|
|
192
131
|
},
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown allows raw HTML anywhere, so a ".md" file is served as HTML with the parts that can never be HTML — front matter, code
|
|
3
|
+
* blocks and code spans — blanked out first. Blanking keeps every offset, so positions still point at the same place in the
|
|
4
|
+
* file.
|
|
5
|
+
*/
|
|
6
|
+
import { markdownToMdast } from "satteri";
|
|
7
|
+
|
|
8
|
+
import type { MdastNode } from "satteri";
|
|
9
|
+
|
|
10
|
+
export interface TextRegion {
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** None of these nest, so the regions they produce never overlap. */
|
|
16
|
+
const NON_HTML_NODES = new Set(["code", "inlineCode", "yaml", "toml"]);
|
|
17
|
+
|
|
18
|
+
/** The regions of a markdown document that must not be treated as HTML, sorted by start offset. */
|
|
19
|
+
export function findMarkdownNonHtmlRegions(text: string): TextRegion[] {
|
|
20
|
+
let tree: MdastNode;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
tree = markdownToMdast(text);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.warn("[staticbolt] could not parse markdown, its whole content is handled as HTML:", error);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const regions: TextRegion[] = [];
|
|
30
|
+
const pending: MdastNode[] = [tree];
|
|
31
|
+
|
|
32
|
+
while (pending.length > 0) {
|
|
33
|
+
const node = pending.pop()!;
|
|
34
|
+
|
|
35
|
+
if (NON_HTML_NODES.has(node.type)) {
|
|
36
|
+
const start = node.position?.start.offset;
|
|
37
|
+
const end = node.position?.end.offset;
|
|
38
|
+
|
|
39
|
+
if (start !== undefined && end !== undefined) {
|
|
40
|
+
regions.push({ start, end });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ("children" in node) {
|
|
47
|
+
pending.push(...node.children);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sorted = regions.toSorted((a, b) => a.start - b.start);
|
|
52
|
+
|
|
53
|
+
return toUtf16Offsets(text, sorted);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The parser counts code points, the editor counts UTF-16 code units. The two only drift apart once a character outside the basic
|
|
58
|
+
* plane, an emoji most of the time, sits before a region.
|
|
59
|
+
*/
|
|
60
|
+
function toUtf16Offsets(text: string, regions: TextRegion[]): TextRegion[] {
|
|
61
|
+
if (!/[\uD800-\uDBFF]/.test(text)) return regions;
|
|
62
|
+
|
|
63
|
+
const astral: number[] = [];
|
|
64
|
+
let codePoint = 0;
|
|
65
|
+
|
|
66
|
+
for (const character of text) {
|
|
67
|
+
if (character.length === 2) {
|
|
68
|
+
astral.push(codePoint);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
codePoint++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const toUtf16 = (offset: number) => offset + astral.filter(position => position < offset).length;
|
|
75
|
+
|
|
76
|
+
return regions.map(region => ({ start: toUtf16(region.start), end: toUtf16(region.end) }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Replaces every region with spaces, keeping the length of the text and its line breaks. Regions must be sorted and disjoint. */
|
|
80
|
+
export function blankRegions(text: string, regions: readonly TextRegion[]): string {
|
|
81
|
+
if (regions.length === 0) return text;
|
|
82
|
+
|
|
83
|
+
let result = "";
|
|
84
|
+
let cursor = 0;
|
|
85
|
+
|
|
86
|
+
for (const region of regions) {
|
|
87
|
+
result += text.slice(cursor, region.start) + text.slice(region.start, region.end).replaceAll(/[^\n\r]/g, " ");
|
|
88
|
+
cursor = region.end;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result + text.slice(cursor);
|
|
92
|
+
}
|