@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.
@@ -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(htmlLanguageService, htmlData);
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(htmlLanguageService, htmlData);
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
- 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";
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
+ }