@staticbolt/lsp 1.0.0-beta.26 → 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 +124 -155
- package/lib/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/helpers/config-loader.ts +14 -12
- package/src/html-server.ts +8 -17
- package/src/modes/embedded-support.ts +40 -172
- package/src/modes/language-modes.ts +8 -69
- package/src/modes/markdown-regions.ts +92 -0
|
@@ -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) {
|
|
@@ -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
|
+
}
|