@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.
@@ -0,0 +1,257 @@
1
+ import path from "node:path";
2
+ import vscodeHtml from "vscode-html-languageservice";
3
+ import { CompletionItemKind, SemanticTokenTypes, TextEdit } from "vscode-languageserver-protocol";
4
+ import * as vscodeUri from "vscode-uri";
5
+
6
+ import { createMergedHtmlDataProvider } from "../helpers/merge-html-data.ts";
7
+ import { isInRegions } from "../helpers/regions.ts";
8
+ import { validateDocument } from "../helpers/validation.ts";
9
+ import { getDocumentContext } from "../helpers/document-context.ts";
10
+ import { StaticboltCode } from "../virtual-code.ts";
11
+
12
+ import type { Project } from "../projects.ts";
13
+ import type { LanguageServiceContext, LanguageServicePlugin, SemanticToken } from "@volar/language-service";
14
+ import type { FileStat, FileSystemProvider, HTMLDataV1, LanguageService } from "vscode-html-languageservice";
15
+ import type { CompletionItem, Position } from "vscode-languageserver-protocol";
16
+ import type { TextDocument } from "vscode-languageserver-textdocument";
17
+
18
+ /** The id the merged data provider registers under with the language service. */
19
+ const DATA_PROVIDER_ID = "staticbolt";
20
+
21
+ /** The cursor inside a `src` or `href` value before any slash, capturing what is typed of its first segment. */
22
+ const PATH_VALUE_START = /(?:src|href)\s*=\s*["']([^"'/\s]*)$/;
23
+
24
+ /** Has the editor open the completions again, right after an item is taken. */
25
+ const SUGGEST = { title: "Suggest", command: "editor.action.triggerSuggest" };
26
+
27
+ /** A document of a loaded project, with the HTML language service that knows the project's tags and attributes. */
28
+ interface Found {
29
+ /** The document's root code, holding its HTML and regions. */
30
+ code: StaticboltCode;
31
+
32
+ /** The project the document belongs to. */
33
+ project: Project;
34
+
35
+ /** The project's HTML language service. */
36
+ languageService: LanguageService;
37
+ }
38
+
39
+ /**
40
+ * What the plugins add to HTML: their tags and attributes for completion and hover, path completion that knows the project's
41
+ * aliases, links resolved the way the build resolves them, and the problems the plugins find. The editor's own HTML support
42
+ * covers the standard elements, and stays out of the regions the plugins embed.
43
+ */
44
+ export function createStaticboltService(): LanguageServicePlugin {
45
+ return {
46
+ name: "staticbolt",
47
+
48
+ capabilities: {
49
+ completionProvider: { triggerCharacters: [".", ":", "<", '"', "=", "/"] },
50
+ hoverProvider: true,
51
+ documentLinkProvider: {},
52
+ diagnosticProvider: { interFileDependencies: false, workspaceDiagnostics: false },
53
+ semanticTokensProvider: { legend: { tokenTypes: [SemanticTokenTypes.operator], tokenModifiers: [] } },
54
+ },
55
+
56
+ create(context) {
57
+ // One language service per project's data, which is a new array whenever its config is loaded again
58
+ const languageServices = new WeakMap<HTMLDataV1[], LanguageService>();
59
+
60
+ /** The HTML language service that knows a project's tags and attributes. */
61
+ function languageServiceOf(project: Project): LanguageService {
62
+ let languageService = languageServices.get(project.htmlData);
63
+
64
+ if (!languageService) {
65
+ languageService = vscodeHtml.getLanguageService({
66
+ clientCapabilities: context.env.clientCapabilities,
67
+ fileSystemProvider: fileSystemOf(context),
68
+ useDefaultDataProvider: false,
69
+ customDataProviders: [createMergedHtmlDataProvider(DATA_PROVIDER_ID, project.htmlData)],
70
+ });
71
+
72
+ languageServices.set(project.htmlData, languageService);
73
+ }
74
+
75
+ return languageService;
76
+ }
77
+
78
+ /** The document's code and project, or nothing when the document is no HTML of a loaded project. */
79
+ function find(document: TextDocument): Found | undefined {
80
+ if (document.languageId !== "html") {
81
+ return undefined;
82
+ }
83
+
84
+ const code = codeOf(context, document);
85
+ if (!code?.project) {
86
+ return undefined;
87
+ }
88
+
89
+ return { code, project: code.project, languageService: languageServiceOf(code.project) };
90
+ }
91
+
92
+ return {
93
+ async provideCompletionItems(document, position) {
94
+ const found = find(document);
95
+ if (!found) {
96
+ return;
97
+ }
98
+
99
+ // Inside a plugin's region the code is the plugin language's, served through TypeScript
100
+ if (isInRegions(found.code.regions, document.offsetAt(position))) {
101
+ return;
102
+ }
103
+
104
+ const { code, project, languageService } = found;
105
+ const documentContext = getDocumentContext(code.uri.toString(), project.resolver);
106
+ const list = await languageService.doComplete2(document, position, code.htmlDocument, documentContext);
107
+
108
+ list.items.push(...aliasCompletions(document, position, project.resolver.aliases));
109
+
110
+ // Ahead of whatever the editor's own HTML support offers
111
+ for (const item of list.items) {
112
+ item.sortText = "0_" + item.label;
113
+ }
114
+
115
+ return list;
116
+ },
117
+
118
+ provideHover(document, position) {
119
+ const found = find(document);
120
+ if (!found) {
121
+ return;
122
+ }
123
+
124
+ if (isInRegions(found.code.regions, document.offsetAt(position))) {
125
+ return;
126
+ }
127
+
128
+ return found.languageService.doHover(document, position, found.code.htmlDocument);
129
+ },
130
+
131
+ provideDocumentLinks(document) {
132
+ const found = find(document);
133
+ if (!found) {
134
+ return;
135
+ }
136
+
137
+ const { code, project, languageService } = found;
138
+ const documentPath = code.uri.fsPath;
139
+ const documentContext = getDocumentContext(code.uri.toString(), project.resolver);
140
+ const links = languageService.findDocumentLinks(document, documentContext);
141
+
142
+ // Resolved the way the build resolves sources: aliases, extensionless paths and directories with an index file
143
+ for (const link of links) {
144
+ if (!link.target) continue;
145
+
146
+ const source = path.relative(path.dirname(documentPath), vscodeUri.URI.parse(link.target).fsPath);
147
+ const resolved = project.resolver.resolve(source, code.file);
148
+ if (!resolved) continue;
149
+
150
+ link.target = vscodeUri.URI.file(resolved.path).toString();
151
+ }
152
+
153
+ return links;
154
+ },
155
+
156
+ /**
157
+ * Colours the delimiters of the plugins' regions, the `{{` and `}}` of a placeholder: they are outside the code, so
158
+ * nothing else colours them, and they would take the colour of whatever they sit in, an attribute's string say.
159
+ */
160
+ provideDocumentSemanticTokens(document, _range, legend) {
161
+ const found = find(document);
162
+ if (!found) {
163
+ return;
164
+ }
165
+
166
+ const type = legend.tokenTypes.indexOf(SemanticTokenTypes.operator);
167
+ const tokens: SemanticToken[] = [];
168
+
169
+ for (const region of found.code.regions) {
170
+ if (!region.extent) continue;
171
+
172
+ for (const [start, end] of [
173
+ [region.extent.start, region.start],
174
+ [region.end, region.extent.end],
175
+ ]) {
176
+ if (end <= start) continue;
177
+
178
+ const { line, character } = document.positionAt(start);
179
+
180
+ tokens.push([line, character, end - start, type, 0]);
181
+ }
182
+ }
183
+
184
+ return tokens;
185
+ },
186
+
187
+ provideDiagnostics(document) {
188
+ const found = find(document);
189
+ if (!found) {
190
+ return;
191
+ }
192
+
193
+ return validateDocument({
194
+ document,
195
+ info: found.code.info,
196
+ project: found.project,
197
+ console: context.env.console ?? console,
198
+ });
199
+ },
200
+ };
201
+ },
202
+ };
203
+ }
204
+
205
+ /** The editor's file system, as the HTML language service reads it for path completions; nothing is there without one. */
206
+ function fileSystemOf(context: LanguageServiceContext): FileSystemProvider {
207
+ const missing: FileStat = { type: vscodeHtml.FileType.Unknown, ctime: -1, mtime: -1, size: -1 };
208
+
209
+ return {
210
+ async stat(uri) {
211
+ return (await context.env.fs?.stat(vscodeUri.URI.parse(uri))) ?? missing;
212
+ },
213
+
214
+ async readDirectory(uri) {
215
+ return (await context.env.fs?.readDirectory(vscodeUri.URI.parse(uri))) ?? [];
216
+ },
217
+ };
218
+ }
219
+
220
+ /** The root code of the document a service is asked about, whether it is the document itself or its embedded HTML copy. */
221
+ function codeOf(context: LanguageServiceContext, document: TextDocument): StaticboltCode | undefined {
222
+ const uri = vscodeUri.URI.parse(document.uri);
223
+ const [sourceUri] = context.decodeEmbeddedDocumentUri(uri) ?? [uri];
224
+ const root = context.language.scripts.get(sourceUri)?.generated?.root;
225
+
226
+ if (!(root instanceof StaticboltCode)) {
227
+ return undefined;
228
+ }
229
+
230
+ return root;
231
+ }
232
+
233
+ /**
234
+ * The path aliases, offered at the start of a `src` or `href` value: a partly typed one completes, and a directory alias opens
235
+ * its listing right away.
236
+ */
237
+ function aliasCompletions(document: TextDocument, position: Position, aliases: Record<string, string>): CompletionItem[] {
238
+ const lineBeforeCursor = document.getText({ start: { line: position.line, character: 0 }, end: position });
239
+ const typed = PATH_VALUE_START.exec(lineBeforeCursor)?.[1];
240
+
241
+ if (typed === undefined) {
242
+ return [];
243
+ }
244
+
245
+ const range = { start: { line: position.line, character: position.character - typed.length }, end: position };
246
+
247
+ return Object.keys(aliases).map(alias => {
248
+ const textEdit = TextEdit.replace(range, alias);
249
+
250
+ // A directory alias goes on to list its files
251
+ if (alias.endsWith("/")) {
252
+ return { label: alias, kind: CompletionItemKind.Folder, textEdit, command: SUGGEST };
253
+ }
254
+
255
+ return { label: alias, kind: CompletionItemKind.File, textEdit };
256
+ });
257
+ }
@@ -0,0 +1,127 @@
1
+ import { SemanticTokenModifiers, SemanticTokenTypes } from "vscode-languageserver-protocol";
2
+ import { URI } from "vscode-uri";
3
+
4
+ import { isInRegions } from "../helpers/regions.ts";
5
+ import { createSyntaxTokenizer, SCOPED_TYPES } from "../helpers/syntax-tokens.ts";
6
+ import { embeddedFileName, StaticboltCode } from "../virtual-code.ts";
7
+
8
+ import type { LanguageRegions } from "../helpers/regions.ts";
9
+ import type { LanguageServiceContext, LanguageServicePlugin, SemanticToken } from "@volar/language-service";
10
+ import type * as ts from "typescript";
11
+ import type { TextDocument } from "vscode-languageserver-textdocument";
12
+
13
+ /** What the TypeScript service shares with the other services. */
14
+ interface TypeScriptProvide {
15
+ /** The language service over the project's files, the embedded codes among them. */
16
+ "typescript/languageService": () => ts.LanguageService;
17
+ }
18
+
19
+ /** An embedded document with the plugin language it belongs to, as TypeScript knows the document. */
20
+ interface Embedded {
21
+ /** The plugin language, with its regions and holes. */
22
+ group: LanguageRegions;
23
+
24
+ /** The embedded document as a TypeScript file. */
25
+ fileName: string;
26
+ }
27
+
28
+ /**
29
+ * Colours what TypeScript's own tokens leave out of the embedded code the editor's grammar cannot see, a placeholder say:
30
+ * keywords, literals, operators and comments, named from TypeScript's parse of it. TypeScript names the identifiers; together
31
+ * they colour the code the way a TypeScript file is coloured. The languages the editor colours itself get only the identifiers.
32
+ * With `isScoped`, the keywords are sent as the scoped types an editor maps to grammar scopes, see `SCOPED_TYPES`; otherwise as
33
+ * the nearest standard types.
34
+ */
35
+ export function createSyntaxTokensService(typescript: typeof ts, isScoped: boolean): LanguageServicePlugin {
36
+ const tokenize = createSyntaxTokenizer(typescript, isScoped);
37
+
38
+ return {
39
+ name: "staticbolt-syntax-tokens",
40
+
41
+ capabilities: {
42
+ semanticTokensProvider: {
43
+ legend: {
44
+ tokenTypes: [...Object.values(SemanticTokenTypes), ...Object.keys(SCOPED_TYPES)],
45
+ tokenModifiers: Object.values(SemanticTokenModifiers),
46
+ },
47
+ },
48
+ },
49
+
50
+ create(context) {
51
+ /**
52
+ * The parsed file of an embedded document as the project's TypeScript holds it, with the checker that knows its symbols, or
53
+ * a fresh parse alone when the project has none.
54
+ */
55
+ function parse(document: TextDocument, fileName: string): [ts.SourceFile, ts.TypeChecker | undefined] {
56
+ const program = context.inject<TypeScriptProvide>("typescript/languageService")?.getProgram();
57
+ const parsed = program?.getSourceFile(fileName);
58
+ if (program && parsed && parsed.text === document.getText()) {
59
+ return [parsed, program.getTypeChecker()];
60
+ }
61
+
62
+ return [typescript.createSourceFile(fileName, document.getText(), typescript.ScriptTarget.Latest, true), undefined];
63
+ }
64
+
65
+ return {
66
+ provideDocumentSemanticTokens(document, _range, legend) {
67
+ if (document.languageId !== "typescript") {
68
+ return;
69
+ }
70
+
71
+ const embedded = embeddedOf(context, document);
72
+ if (!embedded || embedded.group.language.isColouredByEditor) {
73
+ return;
74
+ }
75
+
76
+ const [sourceFile, checker] = parse(document, embedded.fileName);
77
+ const tokens: SemanticToken[] = [];
78
+
79
+ for (const token of tokenize(sourceFile, document, checker)) {
80
+ const type = legend.tokenTypes.indexOf(token.type);
81
+ if (type === -1) continue;
82
+
83
+ // The masks standing in for other languages' code are not code to colour
84
+ const offset = document.offsetAt({ line: token.line, character: token.character });
85
+ if (isInRegions(embedded.group.holes, offset)) continue;
86
+
87
+ let modifiers = 0;
88
+
89
+ for (const modifier of token.modifiers) {
90
+ const bit = legend.tokenModifiers.indexOf(modifier);
91
+ if (bit === -1) continue;
92
+
93
+ modifiers |= 1 << bit;
94
+ }
95
+
96
+ tokens.push([token.line, token.character, token.length, type, modifiers]);
97
+ }
98
+
99
+ return tokens;
100
+ },
101
+ };
102
+ },
103
+ };
104
+ }
105
+
106
+ /** The plugin language an embedded document belongs to and its TypeScript file name, as `getExtraServiceScripts` names it. */
107
+ function embeddedOf(context: LanguageServiceContext, document: TextDocument): Embedded | undefined {
108
+ const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri));
109
+ if (!decoded) {
110
+ return undefined;
111
+ }
112
+
113
+ const [sourceUri, codeId] = decoded;
114
+ const root = context.language.scripts.get(sourceUri)?.generated?.root;
115
+ if (!(root instanceof StaticboltCode)) {
116
+ return undefined;
117
+ }
118
+
119
+ const group = root.languages.find(candidate => candidate.id === codeId);
120
+ if (!group) {
121
+ return undefined;
122
+ }
123
+
124
+ const documentFileName = context.project.typescript?.uriConverter.asFileName(sourceUri) ?? sourceUri.fsPath;
125
+
126
+ return { group, fileName: embeddedFileName(documentFileName, codeId) };
127
+ }
@@ -0,0 +1,20 @@
1
+ import { create } from "volar-service-typescript";
2
+
3
+ import type { LanguageServicePlugin } from "@volar/language-service";
4
+ import type * as ts from "typescript";
5
+
6
+ /**
7
+ * TypeScript's features for the embedded codes, through Volar's TypeScript service: completion, hover, diagnostics, semantic
8
+ * tokens, definitions, references, rename, folding and the rest. Formatting is left out: a document is HTML to the editor, and
9
+ * its own formatter takes care of the whole of it.
10
+ */
11
+ export function createTypeScriptServices(typescript: typeof ts): LanguageServicePlugin[] {
12
+ return create(typescript).map(plugin => ({
13
+ ...plugin,
14
+ capabilities: {
15
+ ...plugin.capabilities,
16
+ documentFormattingProvider: undefined,
17
+ documentOnTypeFormattingProvider: undefined,
18
+ },
19
+ }));
20
+ }
@@ -0,0 +1,196 @@
1
+ import path from "node:path";
2
+ import { Resolver } from "@staticbolt/core";
3
+ import vscodeHtml from "vscode-html-languageservice";
4
+ import { TextDocument } from "vscode-languageserver-textdocument";
5
+
6
+ import { parseElements } from "./helpers/document-elements.ts";
7
+ import { describeDocument } from "./helpers/document-info.ts";
8
+ import { findMarkdownNonHtmlRegions } from "./helpers/markdown-regions.ts";
9
+ import { findPluginRegions, groupByFile } from "./helpers/regions.ts";
10
+ import { blankAround, blankRegions, mask } from "./helpers/virtual-document.ts";
11
+
12
+ import type { LanguageRegions, PluginRegion } from "./helpers/regions.ts";
13
+ import type { Project } from "./projects.ts";
14
+ import type { CodeMapping, IScriptSnapshot, VirtualCode } from "@volar/language-core";
15
+ import type { DocumentInfo, EmbeddedRegion } from "@staticbolt/core";
16
+ import type * as ts from "typescript";
17
+ import type { HTMLDocument, LanguageService } from "vscode-html-languageservice";
18
+ import type { URI } from "vscode-uri";
19
+
20
+ /** The id of the root code, and of the HTML copy a markdown document is served through. */
21
+ const ROOT_ID = "root";
22
+
23
+ /** The id of the embedded code holding the HTML of a document that is not HTML itself. */
24
+ const HTML_ID = "html";
25
+
26
+ /** The TypeScript file name an embedded code of a document is served under. */
27
+ export function embeddedFileName(documentFileName: string, codeId: string): string {
28
+ return `${documentFileName}.${codeId}.ts`;
29
+ }
30
+
31
+ /** What every feature is allowed to do on a mapped stretch of code. */
32
+ const ALL_FEATURES: CodeMapping["data"] = {
33
+ verification: true,
34
+ completion: true,
35
+ semantic: true,
36
+ navigation: true,
37
+ structure: true,
38
+ format: false,
39
+ };
40
+
41
+ /** The HTML language service the codes parse with; no data provider, only the tree is wanted here. */
42
+ const htmlLanguageService: LanguageService = vscodeHtml.getLanguageService({ useDefaultDataProvider: false });
43
+
44
+ /**
45
+ * A document as the server sees it: the HTML (a markdown document's with everything that cannot be HTML blanked out), the regions
46
+ * plugins embed in it, and an embedded TypeScript code per plugin language, served through the project's TypeScript.
47
+ */
48
+ export class StaticboltCode implements VirtualCode {
49
+ /** The root is the document. */
50
+ readonly id = ROOT_ID;
51
+
52
+ /** `html` or `markdown`. */
53
+ readonly languageId: string;
54
+
55
+ /** The document's text. */
56
+ readonly snapshot: IScriptSnapshot;
57
+
58
+ /** The whole document maps onto itself. */
59
+ readonly mappings: CodeMapping[];
60
+
61
+ /** The HTML copy of a markdown document, then the TypeScript code of every plugin language with regions. */
62
+ readonly embeddedCodes: VirtualCode[];
63
+
64
+ /** The document's uri. */
65
+ readonly uri: URI;
66
+
67
+ /** The project the document belongs to, or nothing when it is outside every loaded one. */
68
+ readonly project: Project | undefined;
69
+
70
+ /** The document's path relative to its project, or its whole path outside one. */
71
+ readonly file: string;
72
+
73
+ /** The document as HTML: the text itself, or for markdown the blanked copy. */
74
+ readonly html: string;
75
+
76
+ /** The regions the plugins embed, in text order. */
77
+ readonly regions: PluginRegion[];
78
+
79
+ /** The regions by plugin language. */
80
+ readonly languages: LanguageRegions[];
81
+
82
+ /** The parsed HTML, on first use. */
83
+ #htmlDocument: HTMLDocument | undefined;
84
+
85
+ /** The document as the plugins see it, on first use. */
86
+ #info: DocumentInfo | undefined;
87
+
88
+ constructor(typescript: typeof ts, uri: URI, languageId: string, snapshot: IScriptSnapshot, project: Project | undefined) {
89
+ const text = snapshot.getText(0, snapshot.getLength());
90
+
91
+ this.uri = uri;
92
+ this.languageId = languageId;
93
+ this.snapshot = snapshot;
94
+ this.project = project;
95
+ this.file = project ? path.relative(project.root, uri.fsPath) : uri.fsPath;
96
+ this.mappings = [identityMapping(text.length)];
97
+ this.html = languageId === "markdown" ? blankRegions(text, findMarkdownNonHtmlRegions(text)) : text;
98
+ this.regions = project ? findPluginRegions(this.info, project.embeddedLanguages) : [];
99
+ this.languages = groupByFile(this.regions);
100
+ this.embeddedCodes = this.languages.map(group => createTypeScriptCode(typescript, this.info, group));
101
+
102
+ if (languageId === "markdown") {
103
+ this.embeddedCodes.unshift(createHtmlCode(typescript, this.html));
104
+ }
105
+ }
106
+
107
+ /** The parsed HTML. */
108
+ get htmlDocument(): HTMLDocument {
109
+ this.#htmlDocument ??= htmlLanguageService.parseHTMLDocument(TextDocument.create(this.uri.toString(), "html", 0, this.html));
110
+
111
+ return this.#htmlDocument;
112
+ }
113
+
114
+ /**
115
+ * The document as the plugins see it: its elements with their attributes and where everything sits, and the project's resolver.
116
+ * Outside a project, a resolver of the document's own directory.
117
+ */
118
+ get info(): DocumentInfo {
119
+ this.#info ??= describeDocument(
120
+ this.html,
121
+ this.file,
122
+ parseElements(htmlLanguageService, this.html, this.htmlDocument),
123
+ this.project?.resolver ?? new Resolver(path.dirname(this.uri.fsPath), false, {}, false)
124
+ );
125
+
126
+ return this.#info;
127
+ }
128
+ }
129
+
130
+ /** A mapping of a whole text onto itself. */
131
+ function identityMapping(length: number): CodeMapping {
132
+ return { sourceOffsets: [0], generatedOffsets: [0], lengths: [length], data: ALL_FEATURES };
133
+ }
134
+
135
+ /** A markdown document's HTML copy, at the same offsets. */
136
+ function createHtmlCode(typescript: typeof ts, html: string): VirtualCode {
137
+ return {
138
+ id: HTML_ID,
139
+ languageId: "html",
140
+ snapshot: typescript.ScriptSnapshot.fromString(html),
141
+ mappings: [identityMapping(html.length)],
142
+ };
143
+ }
144
+
145
+ /**
146
+ * The TypeScript code of one file of a plugin language: the document with everything outside the file's regions blanked, so every
147
+ * offset means the same thing in both, the regions of other languages masked, and the language's prelude appended at the end. A
148
+ * module is made one with an `export {}`, so its top level is its own; a script's top level is the global scope, as it is in the
149
+ * browser. The regions map back to the document; what lies before the first and after the last maps onto their edges, so what
150
+ * TypeScript puts at the top or the bottom of the file, an import or a declaration it adds say, lands in the code.
151
+ */
152
+ function createTypeScriptCode(typescript: typeof ts, info: DocumentInfo, group: LanguageRegions): VirtualCode {
153
+ const { id, language, isModule, regions, holes } = group;
154
+ const prelude = typeof language.prelude === "function" ? language.prelude(info) : language.prelude;
155
+ const suffix = [isModule ? "export {};" : "", prelude ?? ""].filter(Boolean).join("\n");
156
+ const text = `${mask(typescript, blankAround(info.text, regions), holes)}\n${suffix}\n`;
157
+ const first = regions[0];
158
+ const last = regions.at(-1) ?? first;
159
+
160
+ return {
161
+ id,
162
+ languageId: "typescript",
163
+ snapshot: typescript.ScriptSnapshot.fromString(text),
164
+ mappings: [
165
+ {
166
+ sourceOffsets: regions.map(region => region.start),
167
+ generatedOffsets: regions.map(region => region.start),
168
+ lengths: regions.map(region => region.end - region.start),
169
+ data: ALL_FEATURES,
170
+ },
171
+ edgeMapping(topOf(info.text, first), 0, first.start),
172
+ edgeMapping(last.end, last.end, text.length - last.end),
173
+ ],
174
+ };
175
+ }
176
+
177
+ /**
178
+ * A stretch of the generated text outside the regions mapped onto one spot of the document, for the edits of completions and code
179
+ * actions only: nothing there is verified, coloured or folded.
180
+ */
181
+ function edgeMapping(sourceOffset: number, generatedOffset: number, generatedLength: number): CodeMapping {
182
+ return {
183
+ sourceOffsets: [sourceOffset],
184
+ generatedOffsets: [generatedOffset],
185
+ lengths: [0],
186
+ generatedLengths: [generatedLength],
187
+ data: { completion: true, navigation: true, verification: false, semantic: false, structure: false, format: false },
188
+ };
189
+ }
190
+
191
+ /** Where the top of a file's code is in the document: the first region's start, past the line break a script body opens with. */
192
+ function topOf(text: string, first: EmbeddedRegion): number {
193
+ const lineBreak = /^\r?\n/.exec(text.slice(first.start, first.end));
194
+
195
+ return first.start + (lineBreak?.[0].length ?? 0);
196
+ }