@zentryc5t/ts-plugin-for-bruno 3.0.6 → 3.2.0

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.
Files changed (54) hide show
  1. package/README.md +4 -0
  2. package/out/codeBlocks/bruCodeBlocks.js +71 -0
  3. package/out/codeBlocks/bruCodeBlocks.js.map +1 -0
  4. package/out/codeChanges/codeChanges.js +144 -0
  5. package/out/codeChanges/codeChanges.js.map +1 -0
  6. package/out/collection/collectionFormat.js +69 -0
  7. package/out/collection/collectionFormat.js.map +1 -0
  8. package/out/constants.js +36 -0
  9. package/out/constants.js.map +1 -0
  10. package/out/diagnostics/filterDefaultDiagnostics.js +81 -0
  11. package/out/diagnostics/filterDefaultDiagnostics.js.map +1 -0
  12. package/out/fileContent.js +28 -0
  13. package/out/fileContent.js.map +1 -0
  14. package/out/fileTypes.js +16 -0
  15. package/out/fileTypes.js.map +1 -0
  16. package/out/index.js +18 -260
  17. package/out/index.js.map +1 -1
  18. package/out/languageService/codeChangeOverrides.js +40 -0
  19. package/out/languageService/codeChangeOverrides.js.map +1 -0
  20. package/out/languageService/diagnosticsOverrides.js +41 -0
  21. package/out/languageService/diagnosticsOverrides.js.map +1 -0
  22. package/out/languageService/formattingOverrides.js +18 -0
  23. package/out/languageService/formattingOverrides.js.map +1 -0
  24. package/out/languageService/navigationOverrides.js +98 -0
  25. package/out/languageService/navigationOverrides.js.map +1 -0
  26. package/out/pluginConfiguration.js +29 -0
  27. package/out/pluginConfiguration.js.map +1 -0
  28. package/out/yamlFiles/getIndentationOfScripts.js +24 -0
  29. package/out/yamlFiles/getIndentationOfScripts.js.map +1 -0
  30. package/out/yamlFiles/maskYamlFileContent.js +126 -0
  31. package/out/yamlFiles/maskYamlFileContent.js.map +1 -0
  32. package/out/yamlFiles/maskYamlFilesForTypescriptServer.js +94 -0
  33. package/out/yamlFiles/maskYamlFilesForTypescriptServer.js.map +1 -0
  34. package/package.json +4 -3
  35. package/src/codeBlocks/bruCodeBlocks.ts +66 -0
  36. package/src/codeChanges/codeChanges.ts +182 -0
  37. package/src/collection/collectionFormat.ts +96 -0
  38. package/src/constants.test.ts +12 -0
  39. package/src/constants.ts +31 -0
  40. package/src/diagnostics/filterDefaultDiagnostics.ts +145 -0
  41. package/src/fileContent.ts +38 -0
  42. package/src/fileTypes.ts +13 -0
  43. package/src/index.test.ts +329 -0
  44. package/src/index.ts +24 -496
  45. package/src/languageService/codeChangeOverrides.ts +90 -0
  46. package/src/languageService/diagnosticsOverrides.ts +65 -0
  47. package/src/languageService/formattingOverrides.ts +42 -0
  48. package/src/languageService/navigationOverrides.ts +201 -0
  49. package/src/pluginConfiguration.ts +34 -0
  50. package/src/yamlFiles/getIndentationOfScripts.ts +34 -0
  51. package/src/yamlFiles/maskYamlFileContent.test.ts +213 -0
  52. package/src/yamlFiles/maskYamlFileContent.ts +179 -0
  53. package/src/yamlFiles/maskYamlFilesForTypescriptServer.ts +85 -0
  54. package/tsconfig.json +2 -1
@@ -0,0 +1,42 @@
1
+ import * as ts from "typescript/lib/tsserverlibrary";
2
+ import { isYamlFile } from "../fileTypes";
3
+
4
+ // The Typescript server only sees the masked content of Yaml files (see `maskYamlFilesForTypescriptServer`).
5
+ // Formatting edits are computed for the masked content and would therefore remove the Yaml content outside of scripts.
6
+ export function overrideFormatting(
7
+ info: ts.server.PluginCreateInfo,
8
+ proxy: ts.LanguageService,
9
+ ) {
10
+ proxy.getFormattingEditsForDocument = (fileName, options) =>
11
+ isYamlFile(fileName)
12
+ ? []
13
+ : info.languageService.getFormattingEditsForDocument(
14
+ fileName,
15
+ options,
16
+ );
17
+
18
+ proxy.getFormattingEditsForRange = (fileName, start, end, options) =>
19
+ isYamlFile(fileName)
20
+ ? []
21
+ : info.languageService.getFormattingEditsForRange(
22
+ fileName,
23
+ start,
24
+ end,
25
+ options,
26
+ );
27
+
28
+ proxy.getFormattingEditsAfterKeystroke = (
29
+ fileName,
30
+ position,
31
+ key,
32
+ options,
33
+ ) =>
34
+ isYamlFile(fileName)
35
+ ? []
36
+ : info.languageService.getFormattingEditsAfterKeystroke(
37
+ fileName,
38
+ position,
39
+ key,
40
+ options,
41
+ );
42
+ }
@@ -0,0 +1,201 @@
1
+ import { extname } from "path";
2
+ import * as ts from "typescript/lib/tsserverlibrary";
3
+ import { isInABrunoCollection } from "../collection/collectionFormat";
4
+ import { getTempJsFileBaseNameWithoutExtension } from "../constants";
5
+ import { hasEmbeddedCodeBlocks } from "../fileTypes";
6
+
7
+ export function overrideHoverAndCompletions(
8
+ info: ts.server.PluginCreateInfo,
9
+ proxy: ts.LanguageService,
10
+ ) {
11
+ // All hovers are provided by the extension implementation for '.bru' and '.yml' files.
12
+ proxy.getQuickInfoAtPosition = (fileName, position) => {
13
+ if (hasEmbeddedCodeBlocks(fileName)) {
14
+ return undefined;
15
+ }
16
+
17
+ return info.languageService.getQuickInfoAtPosition(fileName, position);
18
+ };
19
+
20
+ proxy.getCompletionsAtPosition = (fileName, position, options) => {
21
+ // All completions are provided by the extension implementation for '.bru' and '.yml' files.
22
+ if (hasEmbeddedCodeBlocks(fileName)) {
23
+ return undefined;
24
+ }
25
+
26
+ const defaultCompletions =
27
+ info.languageService.getCompletionsAtPosition(
28
+ fileName,
29
+ position,
30
+ options,
31
+ );
32
+
33
+ return defaultCompletions && isInABrunoCollection(info, fileName)
34
+ ? {
35
+ ...defaultCompletions,
36
+ entries: defaultCompletions.entries.filter(
37
+ ({ data }) =>
38
+ !data ||
39
+ !data.fileName ||
40
+ !data.fileName.includes(
41
+ getTempJsFileBaseNameWithoutExtension(),
42
+ ),
43
+ ),
44
+ }
45
+ : defaultCompletions;
46
+ };
47
+ }
48
+
49
+ export function overrideRename(
50
+ info: ts.server.PluginCreateInfo,
51
+ proxy: ts.LanguageService,
52
+ ) {
53
+ proxy.getRenameInfo = (fileName, position, preferences) => {
54
+ const defaultRenameInfo = info.languageService.getRenameInfo(
55
+ fileName,
56
+ position,
57
+ preferences as ts.UserPreferences,
58
+ );
59
+
60
+ if (
61
+ extname(fileName) != ".js" ||
62
+ !isInABrunoCollection(info, fileName)
63
+ ) {
64
+ return defaultRenameInfo;
65
+ }
66
+
67
+ const definitionForPosition =
68
+ info.languageService.getDefinitionAtPosition(fileName, position);
69
+
70
+ const isForInbuiltRuntimeFunction =
71
+ definitionForPosition &&
72
+ definitionForPosition.some(({ fileName }) =>
73
+ fileName.includes(getTempJsFileBaseNameWithoutExtension()),
74
+ );
75
+
76
+ return isForInbuiltRuntimeFunction
77
+ ? {
78
+ canRename: false,
79
+ localizedErrorMessage:
80
+ "You cannot rename elements that are provided by Bruno at runtime.",
81
+ }
82
+ : defaultRenameInfo;
83
+ };
84
+
85
+ proxy.findRenameLocations = (
86
+ fileName,
87
+ position,
88
+ findInStrings,
89
+ findInComments,
90
+ providePrefixAndSuffixTextForRename,
91
+ ) => {
92
+ const defaultRenameLocations = info.languageService.findRenameLocations(
93
+ fileName,
94
+ position,
95
+ findInStrings,
96
+ findInComments,
97
+ providePrefixAndSuffixTextForRename &&
98
+ typeof providePrefixAndSuffixTextForRename != "boolean"
99
+ ? providePrefixAndSuffixTextForRename
100
+ : {},
101
+ );
102
+
103
+ return defaultRenameLocations && isInABrunoCollection(info, fileName)
104
+ ? defaultRenameLocations.filter(
105
+ ({ fileName: fileForRenameLocation }) =>
106
+ !fileForRenameLocation.includes(
107
+ getTempJsFileBaseNameWithoutExtension(),
108
+ ),
109
+ )
110
+ : defaultRenameLocations;
111
+ };
112
+ }
113
+
114
+ export function overrideReferencesAndDefinitions(
115
+ info: ts.server.PluginCreateInfo,
116
+ proxy: ts.LanguageService,
117
+ ) {
118
+ proxy.findReferences = (fileName, position) => {
119
+ const defaultReferences = info.languageService.findReferences(
120
+ fileName,
121
+ position,
122
+ );
123
+
124
+ return defaultReferences && isInABrunoCollection(info, fileName)
125
+ ? defaultReferences.filter(
126
+ ({ definition: { fileName: fileForDefinition } }) =>
127
+ !fileForDefinition.includes(
128
+ getTempJsFileBaseNameWithoutExtension(),
129
+ ),
130
+ )
131
+ : defaultReferences;
132
+ };
133
+
134
+ proxy.getDefinitionAtPosition = (fileName, position) => {
135
+ const defaultDefinitions = info.languageService.getDefinitionAtPosition(
136
+ fileName,
137
+ position,
138
+ );
139
+
140
+ return defaultDefinitions && isInABrunoCollection(info, fileName)
141
+ ? defaultDefinitions.filter(
142
+ ({ fileName: fileForDefinition }) =>
143
+ !fileForDefinition.includes(
144
+ getTempJsFileBaseNameWithoutExtension(),
145
+ ),
146
+ )
147
+ : defaultDefinitions;
148
+ };
149
+
150
+ proxy.getDefinitionAndBoundSpan = (fileName, position) => {
151
+ const defaultDefinition =
152
+ info.languageService.getDefinitionAndBoundSpan(fileName, position);
153
+
154
+ return defaultDefinition &&
155
+ isInABrunoCollection(info, fileName) &&
156
+ defaultDefinition.definitions
157
+ ? {
158
+ ...defaultDefinition,
159
+ definitions: defaultDefinition.definitions.filter(
160
+ ({ fileName: fileForDefinition }) =>
161
+ !fileForDefinition.includes(
162
+ getTempJsFileBaseNameWithoutExtension(),
163
+ ),
164
+ ),
165
+ }
166
+ : defaultDefinition;
167
+ };
168
+
169
+ proxy.getTypeDefinitionAtPosition = (fileName, position) => {
170
+ const defaultDefinitions = info.languageService.getDefinitionAtPosition(
171
+ fileName,
172
+ position,
173
+ );
174
+
175
+ return defaultDefinitions && isInABrunoCollection(info, fileName)
176
+ ? defaultDefinitions.filter(
177
+ ({ fileName: fileForDefinition }) =>
178
+ !fileForDefinition.includes(
179
+ getTempJsFileBaseNameWithoutExtension(),
180
+ ),
181
+ )
182
+ : defaultDefinitions;
183
+ };
184
+
185
+ proxy.getImplementationAtPosition = (fileName, position) => {
186
+ const defaultImplementations =
187
+ info.languageService.getImplementationAtPosition(
188
+ fileName,
189
+ position,
190
+ );
191
+
192
+ return defaultImplementations && isInABrunoCollection(info, fileName)
193
+ ? defaultImplementations.filter(
194
+ ({ fileName: fileForImplementation }) =>
195
+ !fileForImplementation.includes(
196
+ getTempJsFileBaseNameWithoutExtension(),
197
+ ),
198
+ )
199
+ : defaultImplementations;
200
+ };
201
+ }
@@ -0,0 +1,34 @@
1
+ export interface PluginConfig {
2
+ yamlCollectionSupport: boolean;
3
+ }
4
+
5
+ /**
6
+ * The configuration is passed to the plugin by the VS Code extension (see `configureTsServerPlugin` in the client).
7
+ * As long as no configuration is passed, Yaml collections are not supported.
8
+ */
9
+ export class PluginConfiguration {
10
+ private config: PluginConfig = { yamlCollectionSupport: false };
11
+ private readonly changeListeners: (() => void)[] = [];
12
+
13
+ isYamlCollectionSupportEnabled() {
14
+ return this.config.yamlCollectionSupport;
15
+ }
16
+
17
+ onDidChange(listener: () => void) {
18
+ this.changeListeners.push(listener);
19
+ }
20
+
21
+ update(config: Partial<PluginConfig> | undefined) {
22
+ const yamlCollectionSupport = config?.yamlCollectionSupport == true;
23
+
24
+ if (yamlCollectionSupport == this.config.yamlCollectionSupport) {
25
+ return;
26
+ }
27
+
28
+ this.config = { yamlCollectionSupport };
29
+
30
+ for (const listener of this.changeListeners) {
31
+ listener();
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ import { getScriptContentRanges } from "./maskYamlFileContent";
2
+
3
+ /**
4
+ * Returns the indentation of the scripts that overlap with the given range, if all of them use the same indentation.
5
+ *
6
+ * The indentation of a literal block scalar is the minimal indentation of its non-empty lines.
7
+ * Lines with this indentation are always part of the block scalar, even if it uses an explicit indentation indicator (e.g. `code: |2-`).
8
+ */
9
+ export function getIndentationOfScripts(
10
+ fileName: string,
11
+ fileContent: string,
12
+ range: { start: number; end: number },
13
+ ) {
14
+ const indentations = new Set(
15
+ getScriptContentRanges(fileName, fileContent)
16
+ .filter(
17
+ ({ start, end }) => start <= range.end && range.start <= end,
18
+ )
19
+ .map(({ start, end }) =>
20
+ getMinimalIndentation(fileContent.substring(start, end)),
21
+ ),
22
+ );
23
+
24
+ return indentations.size == 1 ? [...indentations][0] : undefined;
25
+ }
26
+
27
+ function getMinimalIndentation(scriptContent: string) {
28
+ const indentations = scriptContent
29
+ .split(/\r\n|\r|\n|\u2028|\u2029/)
30
+ .filter((line) => line.trim().length > 0)
31
+ .map((line) => /^ */.exec(line)![0].length);
32
+
33
+ return indentations.length > 0 ? Math.min(...indentations) : undefined;
34
+ }
@@ -0,0 +1,213 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import * as ts from "typescript";
3
+ import {
4
+ maskYamlCollectionFileContent,
5
+ maskYamlFileContent,
6
+ } from "./maskYamlFileContent";
7
+
8
+ describe("maskYamlCollectionFileContent", () => {
9
+ it("keeps only the code of scripts in request files and wraps each script in a block", () => {
10
+ const content = `info:
11
+ name: test
12
+ runtime:
13
+ scripts:
14
+ - type: before-request
15
+ code: |-
16
+ const a = 1;
17
+ - type: tests
18
+ code: |
19
+ test("x", () => {});
20
+ `;
21
+
22
+ expect(
23
+ maskYamlCollectionFileContent("request.yml", content).split("\n"),
24
+ ).toEqual([
25
+ blank("info:"),
26
+ blank(" name: test"),
27
+ blank("runtime:"),
28
+ blank(" scripts:"),
29
+ blank(" - type: before-request"),
30
+ blank(" code: |") + "{",
31
+ " const a = 1;",
32
+ "}" + blank(" - type: tests"),
33
+ blank(" code: ") + "{",
34
+ ' test("x", () => {});',
35
+ "}",
36
+ ]);
37
+ });
38
+
39
+ it("keeps the code of scripts in folder and collection settings files", () => {
40
+ const content = `meta:
41
+ name: folder
42
+ request:
43
+ scripts:
44
+ - type: after-response
45
+ code: |-
46
+ bru.setVar("a", res.status);`;
47
+
48
+ expect(
49
+ maskYamlCollectionFileContent("folder.yml", content).split("\n"),
50
+ ).toEqual([
51
+ blank("meta:"),
52
+ blank(" name: folder"),
53
+ blank("request:"),
54
+ blank(" scripts:"),
55
+ blank(" - type: after-response"),
56
+ blank(" code: |") + "{",
57
+ ' bru.setVar("a", res.status);',
58
+ "}",
59
+ ]);
60
+ });
61
+
62
+ it("keeps the code of scripts in collection settings files", () => {
63
+ const content = `opencollection: 1.0.0
64
+ request:
65
+ scripts:
66
+ - type: before-request
67
+ code: |-
68
+ bru.setVar("a", 1);
69
+ `;
70
+
71
+ expect(
72
+ maskYamlCollectionFileContent("opencollection.yml", content).split(
73
+ "\n",
74
+ ),
75
+ ).toEqual([
76
+ blank("opencollection: 1.0.0"),
77
+ blank("request:"),
78
+ blank(" scripts:"),
79
+ blank(" - type: before-request"),
80
+ blank(" code: |") + "{",
81
+ ' bru.setVar("a", 1);',
82
+ "}",
83
+ ]);
84
+ });
85
+
86
+ it("does not add the closing bracket of the last script to a line comment at the end of the file", () => {
87
+ const content = `runtime:
88
+ scripts:
89
+ - type: tests
90
+ code: |-
91
+ test(); // comment`;
92
+
93
+ const sourceFile = ts.createSourceFile(
94
+ "test.ts",
95
+ maskYamlCollectionFileContent("request.yml", content),
96
+ ts.ScriptTarget.Latest,
97
+ );
98
+
99
+ expect(getSyntacticDiagnostics(sourceFile)).toEqual([]);
100
+ });
101
+
102
+ it("masks scripts with a missing or invalid type", () => {
103
+ const content = `runtime:
104
+ scripts:
105
+ - code: |-
106
+ const a = 1;
107
+ - type: before-requset
108
+ code: |-
109
+ const b = 2;
110
+ `;
111
+
112
+ expect(
113
+ maskYamlCollectionFileContent("request.yml", content).trim(),
114
+ ).toBe("");
115
+ });
116
+
117
+ it.each([
118
+ { fileName: "request.yml", maskedSection: "request" },
119
+ { fileName: "folder.yml", maskedSection: "runtime" },
120
+ ])(
121
+ "masks scripts in the '$maskedSection' section for $fileName",
122
+ ({ fileName, maskedSection }) => {
123
+ const content = `${maskedSection}:
124
+ scripts:
125
+ - type: tests
126
+ code: |-
127
+ test();
128
+ `;
129
+
130
+ expect(
131
+ maskYamlCollectionFileContent(fileName, content).trim(),
132
+ ).toBe("");
133
+ },
134
+ );
135
+
136
+ it("keeps line breaks of all kinds", () => {
137
+ const content =
138
+ "runtime:\r\n scripts:\r\n - type: tests\r\n code: |-\r\n test();\r\n";
139
+
140
+ expect(
141
+ maskYamlCollectionFileContent("request.yml", content).split("\r\n"),
142
+ ).toEqual([
143
+ blank("runtime:"),
144
+ blank(" scripts:"),
145
+ blank(" - type: tests"),
146
+ blank(" code: |") + "{",
147
+ " test();",
148
+ "}",
149
+ ]);
150
+ });
151
+
152
+ it("masks scripts that are not defined as literal block scalars", () => {
153
+ const content = `runtime:
154
+ scripts:
155
+ - type: before-request
156
+ code: "bru.setVar('a', 1);"
157
+ - type: tests
158
+ code: >-
159
+ test("x", () => {});
160
+ `;
161
+
162
+ const maskedContent = maskYamlCollectionFileContent(
163
+ "request.yml",
164
+ content,
165
+ );
166
+
167
+ expect(maskedContent).toHaveLength(content.length);
168
+ expect(maskedContent.trim()).toBe("");
169
+ });
170
+
171
+ it("scopes declarations to the script they are defined in", () => {
172
+ const content = `runtime:
173
+ scripts:
174
+ - type: before-request
175
+ code: |-
176
+ const a = 1;
177
+ - type: after-response
178
+ code: |-
179
+ const a = 2;
180
+ `;
181
+
182
+ const sourceFile = ts.createSourceFile(
183
+ "test.ts",
184
+ maskYamlCollectionFileContent("request.yml", content),
185
+ ts.ScriptTarget.Latest,
186
+ );
187
+
188
+ expect(sourceFile.statements.map(({ kind }) => kind)).toEqual([
189
+ ts.SyntaxKind.Block,
190
+ ts.SyntaxKind.Block,
191
+ ]);
192
+ });
193
+ });
194
+
195
+ describe("maskYamlFileContent", () => {
196
+ it("replaces all characters except for line breaks by spaces", () => {
197
+ expect(maskYamlFileContent("a: 1\r\nb:\n - c\n")).toBe(
198
+ `${blank("a: 1")}\r\n${blank("b:")}\n${blank(" - c")}\n`,
199
+ );
200
+ });
201
+ });
202
+
203
+ function getSyntacticDiagnostics(sourceFile: ts.SourceFile) {
204
+ return (
205
+ sourceFile as unknown as {
206
+ parseDiagnostics: ts.DiagnosticWithLocation[];
207
+ }
208
+ ).parseDiagnostics;
209
+ }
210
+
211
+ function blank(text: string) {
212
+ return " ".repeat(text.length);
213
+ }
@@ -0,0 +1,179 @@
1
+ import { basename } from "path";
2
+ import { Document, isMap, isScalar, isSeq, parseDocument, Scalar } from "yaml";
3
+ import { ScriptType } from "../constants";
4
+
5
+ /**
6
+ * Returns the content of a Yaml file from a Bruno collection the way it should be seen by the Typescript server.
7
+ *
8
+ * All characters that are not part of a script's code are replaced by spaces.
9
+ * Line breaks are kept, so that all offsets and line / character positions are equal to the ones in the original content.
10
+ * Each script is wrapped in a block (`{ ... }`), so that its declarations are scoped like the ones in code blocks of `.bru` files.
11
+ *
12
+ * Only the scripts returned by `getScriptContentRanges` are kept.
13
+ */
14
+ export function maskYamlCollectionFileContent(
15
+ fileName: string,
16
+ fileContent: string,
17
+ ) {
18
+ const maskedCharacters = maskYamlFileContent(fileContent).split("");
19
+ const scriptRanges = getScriptContentRanges(fileName, fileContent);
20
+ let firstFreeIndex = 0;
21
+ let suffix = "";
22
+
23
+ for (const [index, { start, end }] of scriptRanges.entries()) {
24
+ const isLastScript = index == scriptRanges.length - 1;
25
+
26
+ const openingBracketIndex = findLastIndexWithoutLineBreak(
27
+ fileContent,
28
+ firstFreeIndex,
29
+ start,
30
+ );
31
+ const closingBracketIndex = findFirstIndexWithoutLineBreak(
32
+ fileContent,
33
+ end,
34
+ isLastScript ? fileContent.length : scriptRanges[index + 1].start,
35
+ );
36
+
37
+ if (
38
+ openingBracketIndex == undefined ||
39
+ (closingBracketIndex == undefined && !isLastScript)
40
+ ) {
41
+ continue;
42
+ }
43
+
44
+ for (let i = start; i < end; i++) {
45
+ maskedCharacters[i] = fileContent[i];
46
+ }
47
+
48
+ maskedCharacters[openingBracketIndex] = "{";
49
+
50
+ if (closingBracketIndex == undefined) {
51
+ // Appending characters at the end does not shift any positions.
52
+ // A line break is needed if the script ends at the end of the file, since the closing bracket could otherwise be part of a line comment.
53
+ suffix = isLineBreak(fileContent[fileContent.length - 1])
54
+ ? "}"
55
+ : "\n}";
56
+ } else {
57
+ maskedCharacters[closingBracketIndex] = "}";
58
+ firstFreeIndex = closingBracketIndex + 1;
59
+ }
60
+ }
61
+
62
+ return maskedCharacters.join("").concat(suffix);
63
+ }
64
+
65
+ /**
66
+ * Replaces all characters except for line breaks by spaces, so that the Typescript server does not provide any language features for the file.
67
+ */
68
+ export function maskYamlFileContent(fileContent: string) {
69
+ return fileContent.replace(/[^\r\n\u2028\u2029]/g, " ");
70
+ }
71
+
72
+ /**
73
+ * Scripts are defined in `runtime.scripts` for request files and in `request.scripts` for folder and collection settings files.
74
+ *
75
+ * The same scripts are returned as by `getScriptBlocksFromYamlFile` in the shared folder of the extension:
76
+ * Only scripts with a valid type that are defined as literal block scalars (`code: |`) are included.
77
+ * For all other scalar styles, the raw text in the file is not equal to the code (e.g. due to quotes or line folding).
78
+ */
79
+ export function getScriptContentRanges(fileName: string, fileContent: string) {
80
+ const document = parseDocument(fileContent);
81
+ const scripts = document.getIn(
82
+ isSettingsFile(fileName, document)
83
+ ? ["request", "scripts"]
84
+ : ["runtime", "scripts"],
85
+ true,
86
+ );
87
+
88
+ return (isSeq(scripts) ? scripts.items : [])
89
+ .flatMap((script) => {
90
+ if (!isMap(script) || !isValidScriptType(script.get("type"))) {
91
+ return [];
92
+ }
93
+
94
+ const code = script.get("code", true);
95
+
96
+ if (
97
+ !isScalar(code) ||
98
+ code.type != Scalar.BLOCK_LITERAL ||
99
+ !code.range
100
+ ) {
101
+ return [];
102
+ }
103
+
104
+ const [headerStart, valueEnd] = code.range;
105
+ const range = getContentRangeForBlockScalar(
106
+ fileContent,
107
+ headerStart,
108
+ valueEnd,
109
+ );
110
+
111
+ return range ? [range] : [];
112
+ })
113
+ .sort(({ start: start1 }, { start: start2 }) => start1 - start2);
114
+ }
115
+
116
+ /**
117
+ * Folder settings files are always named `folder.yml`.
118
+ * The collection settings file is the only file that defines the top-level key `opencollection`.
119
+ */
120
+ function isSettingsFile(fileName: string, document: Document) {
121
+ return basename(fileName) == "folder.yml" || document.has("opencollection");
122
+ }
123
+
124
+ function isValidScriptType(type: unknown) {
125
+ return (Object.values(ScriptType) as unknown[]).includes(type);
126
+ }
127
+
128
+ /**
129
+ * The range of a block scalar node starts with the header (e.g. `|-`), which is followed by the content on the next lines.
130
+ */
131
+ function getContentRangeForBlockScalar(
132
+ fileContent: string,
133
+ headerStart: number,
134
+ valueEnd: number,
135
+ ) {
136
+ const lineBreakAfterHeader = /\r\n|\r|\n|\u2028|\u2029/g;
137
+ lineBreakAfterHeader.lastIndex = headerStart;
138
+ const match = lineBreakAfterHeader.exec(fileContent);
139
+
140
+ if (!match) {
141
+ return undefined;
142
+ }
143
+
144
+ const start = match.index + match[0].length;
145
+
146
+ return start < valueEnd ? { start, end: valueEnd } : undefined;
147
+ }
148
+
149
+ function findLastIndexWithoutLineBreak(
150
+ text: string,
151
+ startIndex: number,
152
+ endIndexExclusive: number,
153
+ ) {
154
+ for (let i = endIndexExclusive - 1; i >= startIndex; i--) {
155
+ if (!isLineBreak(text[i])) {
156
+ return i;
157
+ }
158
+ }
159
+
160
+ return undefined;
161
+ }
162
+
163
+ function findFirstIndexWithoutLineBreak(
164
+ text: string,
165
+ startIndex: number,
166
+ endIndexExclusive: number,
167
+ ) {
168
+ for (let i = startIndex; i < endIndexExclusive; i++) {
169
+ if (!isLineBreak(text[i])) {
170
+ return i;
171
+ }
172
+ }
173
+
174
+ return undefined;
175
+ }
176
+
177
+ function isLineBreak(character: string) {
178
+ return ["\r", "\n", "\u2028", "\u2029"].includes(character);
179
+ }