@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.
- package/README.md +4 -0
- package/out/codeBlocks/bruCodeBlocks.js +71 -0
- package/out/codeBlocks/bruCodeBlocks.js.map +1 -0
- package/out/codeChanges/codeChanges.js +144 -0
- package/out/codeChanges/codeChanges.js.map +1 -0
- package/out/collection/collectionFormat.js +69 -0
- package/out/collection/collectionFormat.js.map +1 -0
- package/out/constants.js +36 -0
- package/out/constants.js.map +1 -0
- package/out/diagnostics/filterDefaultDiagnostics.js +81 -0
- package/out/diagnostics/filterDefaultDiagnostics.js.map +1 -0
- package/out/fileContent.js +28 -0
- package/out/fileContent.js.map +1 -0
- package/out/fileTypes.js +16 -0
- package/out/fileTypes.js.map +1 -0
- package/out/index.js +18 -260
- package/out/index.js.map +1 -1
- package/out/languageService/codeChangeOverrides.js +40 -0
- package/out/languageService/codeChangeOverrides.js.map +1 -0
- package/out/languageService/diagnosticsOverrides.js +41 -0
- package/out/languageService/diagnosticsOverrides.js.map +1 -0
- package/out/languageService/formattingOverrides.js +18 -0
- package/out/languageService/formattingOverrides.js.map +1 -0
- package/out/languageService/navigationOverrides.js +98 -0
- package/out/languageService/navigationOverrides.js.map +1 -0
- package/out/pluginConfiguration.js +29 -0
- package/out/pluginConfiguration.js.map +1 -0
- package/out/yamlFiles/getIndentationOfScripts.js +24 -0
- package/out/yamlFiles/getIndentationOfScripts.js.map +1 -0
- package/out/yamlFiles/maskYamlFileContent.js +126 -0
- package/out/yamlFiles/maskYamlFileContent.js.map +1 -0
- package/out/yamlFiles/maskYamlFilesForTypescriptServer.js +94 -0
- package/out/yamlFiles/maskYamlFilesForTypescriptServer.js.map +1 -0
- package/package.json +4 -3
- package/src/codeBlocks/bruCodeBlocks.ts +66 -0
- package/src/codeChanges/codeChanges.ts +182 -0
- package/src/collection/collectionFormat.ts +96 -0
- package/src/constants.test.ts +12 -0
- package/src/constants.ts +31 -0
- package/src/diagnostics/filterDefaultDiagnostics.ts +145 -0
- package/src/fileContent.ts +38 -0
- package/src/fileTypes.ts +13 -0
- package/src/index.test.ts +329 -0
- package/src/index.ts +24 -496
- package/src/languageService/codeChangeOverrides.ts +90 -0
- package/src/languageService/diagnosticsOverrides.ts +65 -0
- package/src/languageService/formattingOverrides.ts +42 -0
- package/src/languageService/navigationOverrides.ts +201 -0
- package/src/pluginConfiguration.ts +34 -0
- package/src/yamlFiles/getIndentationOfScripts.ts +34 -0
- package/src/yamlFiles/maskYamlFileContent.test.ts +213 -0
- package/src/yamlFiles/maskYamlFileContent.ts +179 -0
- package/src/yamlFiles/maskYamlFilesForTypescriptServer.ts +85 -0
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { extname } from "path";
|
|
2
|
+
import * as ts from "typescript/lib/tsserverlibrary";
|
|
3
|
+
import { getCodeBlockIndizesForBruFile } from "../codeBlocks/bruCodeBlocks";
|
|
4
|
+
import { isInABrunoCollection } from "../collection/collectionFormat";
|
|
5
|
+
import {
|
|
6
|
+
getNamesForInbuiltRuntimeVarsAndFunctions,
|
|
7
|
+
getTempJsFileBaseNameWithoutExtension,
|
|
8
|
+
} from "../constants";
|
|
9
|
+
import { getFileContent } from "../fileContent";
|
|
10
|
+
import { isBruFile, isYamlFile } from "../fileTypes";
|
|
11
|
+
|
|
12
|
+
type AnyDiagnostic = ts.Diagnostic | ts.DiagnosticWithLocation;
|
|
13
|
+
|
|
14
|
+
export function filterDefaultDiagnostics(
|
|
15
|
+
info: ts.server.PluginCreateInfo,
|
|
16
|
+
fileName: string,
|
|
17
|
+
defaultDiagnostics: AnyDiagnostic[],
|
|
18
|
+
) {
|
|
19
|
+
if (isYamlFile(fileName)) {
|
|
20
|
+
return filterDiagnosticsForYamlFile(info, fileName, defaultDiagnostics);
|
|
21
|
+
} else if (!isBruFile(fileName)) {
|
|
22
|
+
return extname(fileName) == ".js"
|
|
23
|
+
? filterDiagnosticsForJsFile(info, fileName, defaultDiagnostics)
|
|
24
|
+
: defaultDiagnostics;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const fileContent = getFileContent(info, fileName);
|
|
28
|
+
|
|
29
|
+
if (!fileContent) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const indizes = getCodeBlockIndizesForBruFile(fileContent);
|
|
34
|
+
|
|
35
|
+
if (indizes.length == 0) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return filterOutDiagnosticsForInbuiltRuntimeFunctions(
|
|
40
|
+
defaultDiagnostics.filter(
|
|
41
|
+
({ start }) =>
|
|
42
|
+
start != undefined &&
|
|
43
|
+
indizes.some(
|
|
44
|
+
({ startIndex: blockStart, endIndex: blockEnd }) =>
|
|
45
|
+
start >= blockStart && start <= blockEnd,
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
fileContent,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The Typescript server only sees the code of scripts within Yaml files (see `maskYamlFilesForTypescriptServer`).
|
|
54
|
+
* Therefore, all diagnostics are already located within code blocks.
|
|
55
|
+
*/
|
|
56
|
+
function filterDiagnosticsForYamlFile(
|
|
57
|
+
info: ts.server.PluginCreateInfo,
|
|
58
|
+
fileName: string,
|
|
59
|
+
defaultDiagnostics: AnyDiagnostic[],
|
|
60
|
+
) {
|
|
61
|
+
const fileContent = getFileContent(info, fileName);
|
|
62
|
+
|
|
63
|
+
return fileContent
|
|
64
|
+
? filterOutDiagnosticsForInbuiltRuntimeFunctions(
|
|
65
|
+
defaultDiagnostics,
|
|
66
|
+
fileContent,
|
|
67
|
+
)
|
|
68
|
+
: [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function filterDiagnosticsForJsFile(
|
|
72
|
+
info: ts.server.PluginCreateInfo,
|
|
73
|
+
fileName: string,
|
|
74
|
+
defaultDiagnostics: AnyDiagnostic[],
|
|
75
|
+
) {
|
|
76
|
+
if (fileName.includes(getTempJsFileBaseNameWithoutExtension())) {
|
|
77
|
+
// Filter out all diagnostics for temp js files.
|
|
78
|
+
return [];
|
|
79
|
+
} else if (!isInABrunoCollection(info, fileName)) {
|
|
80
|
+
return defaultDiagnostics;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const fileContent = getFileContent(info, fileName);
|
|
84
|
+
|
|
85
|
+
if (!fileContent) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const filteredDiagnostics = filterOutDiagnosticsForInbuiltRuntimeFunctions(
|
|
90
|
+
defaultDiagnostics,
|
|
91
|
+
fileContent,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return filteredDiagnostics.map((diagnostic) => {
|
|
95
|
+
const { relatedInformation } = diagnostic;
|
|
96
|
+
|
|
97
|
+
const referencesTempJsFile =
|
|
98
|
+
relatedInformation &&
|
|
99
|
+
relatedInformation.some(
|
|
100
|
+
({ file }) =>
|
|
101
|
+
file &&
|
|
102
|
+
file.fileName.endsWith(
|
|
103
|
+
`${getTempJsFileBaseNameWithoutExtension()}.js`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return referencesTempJsFile
|
|
108
|
+
? { ...diagnostic, relatedInformation: undefined }
|
|
109
|
+
: diagnostic;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function filterOutDiagnosticsForInbuiltRuntimeFunctions(
|
|
114
|
+
diagnosticsToFilter: AnyDiagnostic[],
|
|
115
|
+
fileContent: string,
|
|
116
|
+
) {
|
|
117
|
+
return filterOutMisleadingDiagnosticsForOtherTestFrameworks(
|
|
118
|
+
diagnosticsToFilter,
|
|
119
|
+
).filter(
|
|
120
|
+
({ start, length }) =>
|
|
121
|
+
start != undefined &&
|
|
122
|
+
length != undefined &&
|
|
123
|
+
// Do not show diagnostics for functions that are provided by Bruno at runtime
|
|
124
|
+
!getNamesForInbuiltRuntimeVarsAndFunctions().includes(
|
|
125
|
+
fileContent.substring(start, start + length),
|
|
126
|
+
) &&
|
|
127
|
+
// Avoid showing incorrect error when using `require`
|
|
128
|
+
// (the error seems to only occur for short periods of time when typescript type definitions have not been reloaded for a while)
|
|
129
|
+
fileContent.substring(start, start + length) != "require",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function filterOutMisleadingDiagnosticsForOtherTestFrameworks(
|
|
134
|
+
diagnostics: AnyDiagnostic[],
|
|
135
|
+
) {
|
|
136
|
+
return diagnostics.filter(({ code, messageText }) => {
|
|
137
|
+
const errorText =
|
|
138
|
+
typeof messageText == "string"
|
|
139
|
+
? messageText
|
|
140
|
+
: messageText.messageText;
|
|
141
|
+
|
|
142
|
+
// When using global type definitions for Jest, the TS server expects every `expect` statement to use the matchers for Jest.
|
|
143
|
+
return code != 2_339 || !errorText.includes("JestMatchers<any>");
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as ts from "typescript/lib/tsserverlibrary";
|
|
2
|
+
|
|
3
|
+
export const originalSnapshotGettersForHosts = new WeakMap<
|
|
4
|
+
ts.LanguageServiceHost,
|
|
5
|
+
(fileName: string) => ts.IScriptSnapshot | undefined
|
|
6
|
+
>();
|
|
7
|
+
|
|
8
|
+
export function getFileContent(
|
|
9
|
+
info: ts.server.PluginCreateInfo,
|
|
10
|
+
fileName: string,
|
|
11
|
+
) {
|
|
12
|
+
const scriptSnapshot = info.languageServiceHost.getScriptSnapshot(fileName);
|
|
13
|
+
|
|
14
|
+
if (scriptSnapshot) {
|
|
15
|
+
return scriptSnapshot.getText(0, scriptSnapshot.getLength());
|
|
16
|
+
} else {
|
|
17
|
+
return info.languageServiceHost.readFile(fileName);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* For Yaml files, the content returned by the language service host is masked (see `maskYamlFilesForTypescriptServer`).
|
|
23
|
+
*/
|
|
24
|
+
export function getOriginalFileContent(
|
|
25
|
+
info: ts.server.PluginCreateInfo,
|
|
26
|
+
fileName: string,
|
|
27
|
+
) {
|
|
28
|
+
const getOriginalSnapshot = originalSnapshotGettersForHosts.get(
|
|
29
|
+
info.languageServiceHost,
|
|
30
|
+
);
|
|
31
|
+
const scriptSnapshot = getOriginalSnapshot
|
|
32
|
+
? getOriginalSnapshot(fileName)
|
|
33
|
+
: info.languageServiceHost.getScriptSnapshot(fileName);
|
|
34
|
+
|
|
35
|
+
return scriptSnapshot
|
|
36
|
+
? scriptSnapshot.getText(0, scriptSnapshot.getLength())
|
|
37
|
+
: info.languageServiceHost.readFile(fileName);
|
|
38
|
+
}
|
package/src/fileTypes.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { extname } from "path";
|
|
2
|
+
|
|
3
|
+
export function hasEmbeddedCodeBlocks(fileName: string) {
|
|
4
|
+
return isBruFile(fileName) || isYamlFile(fileName);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isBruFile(fileName: string) {
|
|
8
|
+
return extname(fileName) == ".bru";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isYamlFile(fileName: string) {
|
|
12
|
+
return [".yml", ".yaml"].includes(extname(fileName));
|
|
13
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, it, expect } from "@jest/globals";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
import * as ts from "typescript/lib/tsserverlibrary";
|
|
4
|
+
import init = require("./index");
|
|
5
|
+
import { PluginConfig } from "./pluginConfiguration";
|
|
6
|
+
|
|
7
|
+
const helperModule = `function helper() {
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = { helper };
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
describe("code fixes and refactorings", () => {
|
|
15
|
+
describe.each([
|
|
16
|
+
{
|
|
17
|
+
format: "bru",
|
|
18
|
+
collectionRootFile: {
|
|
19
|
+
name: "bruno.json",
|
|
20
|
+
content: '{ "type": "collection" }',
|
|
21
|
+
},
|
|
22
|
+
requestFileName: "request.bru",
|
|
23
|
+
indentationForExtractedConstant: { spaces: " ", tabs: "\t" },
|
|
24
|
+
requestFileContent: `meta {
|
|
25
|
+
name: request
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
script:pre-request {
|
|
29
|
+
const value = 1;
|
|
30
|
+
const text = "a" + value;
|
|
31
|
+
valeu;
|
|
32
|
+
helper();
|
|
33
|
+
}
|
|
34
|
+
`,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
format: "yaml",
|
|
38
|
+
collectionRootFile: {
|
|
39
|
+
name: "opencollection.yml",
|
|
40
|
+
content: "opencollection: 1.0.0\n",
|
|
41
|
+
},
|
|
42
|
+
requestFileName: "request.yml",
|
|
43
|
+
// A line with less indentation than the script would end the Yaml block scalar. Tabs are not allowed for indenting Yaml content.
|
|
44
|
+
indentationForExtractedConstant: {
|
|
45
|
+
spaces: " ",
|
|
46
|
+
tabs: " ",
|
|
47
|
+
},
|
|
48
|
+
requestFileContent: `info:
|
|
49
|
+
name: request
|
|
50
|
+
runtime:
|
|
51
|
+
scripts:
|
|
52
|
+
- type: before-request
|
|
53
|
+
code: |-
|
|
54
|
+
const value = 1;
|
|
55
|
+
const text = "a" + value;
|
|
56
|
+
valeu;
|
|
57
|
+
helper();
|
|
58
|
+
- type: tests
|
|
59
|
+
code: |-
|
|
60
|
+
test("x", () => {});
|
|
61
|
+
`,
|
|
62
|
+
},
|
|
63
|
+
])(
|
|
64
|
+
"for $format files",
|
|
65
|
+
({
|
|
66
|
+
collectionRootFile,
|
|
67
|
+
requestFileName,
|
|
68
|
+
indentationForExtractedConstant,
|
|
69
|
+
requestFileContent,
|
|
70
|
+
}) => {
|
|
71
|
+
const collectionFiles = {
|
|
72
|
+
[`/collection/${collectionRootFile.name}`]:
|
|
73
|
+
collectionRootFile.content,
|
|
74
|
+
"/collection/lib/helper.js": helperModule,
|
|
75
|
+
[`/collection/${requestFileName}`]: requestFileContent,
|
|
76
|
+
};
|
|
77
|
+
const requestFile = `/collection/${requestFileName}`;
|
|
78
|
+
|
|
79
|
+
it("provides code fixes that only change code within code blocks", () => {
|
|
80
|
+
const languageService =
|
|
81
|
+
createLanguageServiceWithPlugin(collectionFiles);
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
getCodeFixNames(
|
|
85
|
+
languageService,
|
|
86
|
+
requestFile,
|
|
87
|
+
requestFileContent,
|
|
88
|
+
"valeu",
|
|
89
|
+
),
|
|
90
|
+
).toEqual(["spelling"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("does not provide code fixes that insert code outside of code blocks", () => {
|
|
94
|
+
const languageService =
|
|
95
|
+
createLanguageServiceWithPlugin(collectionFiles);
|
|
96
|
+
|
|
97
|
+
// Without filtering, an import would be added at the top of the file and a function declaration at the end of the file.
|
|
98
|
+
expect(
|
|
99
|
+
getCodeFixNames(
|
|
100
|
+
languageService,
|
|
101
|
+
requestFile,
|
|
102
|
+
requestFileContent,
|
|
103
|
+
"helper",
|
|
104
|
+
),
|
|
105
|
+
).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("provides edits for refactorings that only change code within code blocks", () => {
|
|
109
|
+
const languageService =
|
|
110
|
+
createLanguageServiceWithPlugin(collectionFiles);
|
|
111
|
+
|
|
112
|
+
expect(
|
|
113
|
+
getEditsForRefactor(
|
|
114
|
+
languageService,
|
|
115
|
+
requestFile,
|
|
116
|
+
getSelection(requestFileContent, '"a" + value'),
|
|
117
|
+
"Convert to template string",
|
|
118
|
+
),
|
|
119
|
+
).toBeDefined();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("does not provide edits for refactorings that insert code outside of code blocks", () => {
|
|
123
|
+
const languageService =
|
|
124
|
+
createLanguageServiceWithPlugin(collectionFiles);
|
|
125
|
+
|
|
126
|
+
// Without filtering, the function would be added at the end of the file.
|
|
127
|
+
expect(
|
|
128
|
+
getEditsForRefactor(
|
|
129
|
+
languageService,
|
|
130
|
+
requestFile,
|
|
131
|
+
getSelection(requestFileContent, '"a" + value'),
|
|
132
|
+
"Extract to function in global scope",
|
|
133
|
+
),
|
|
134
|
+
).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it.each([
|
|
138
|
+
{ indentationStyle: "spaces", convertTabsToSpaces: true },
|
|
139
|
+
{ indentationStyle: "tabs", convertTabsToSpaces: false },
|
|
140
|
+
] as const)(
|
|
141
|
+
"provides edits for extracting constants that keep the indentation of the code block when indenting with $indentationStyle",
|
|
142
|
+
({ indentationStyle, convertTabsToSpaces }) => {
|
|
143
|
+
const languageService =
|
|
144
|
+
createLanguageServiceWithPlugin(collectionFiles);
|
|
145
|
+
|
|
146
|
+
const edits = getEditsForRefactor(
|
|
147
|
+
languageService,
|
|
148
|
+
requestFile,
|
|
149
|
+
getSelection(requestFileContent, '"a" + value'),
|
|
150
|
+
"Extract to constant in enclosing scope",
|
|
151
|
+
{ convertTabsToSpaces },
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
edits?.edits.flatMap(({ textChanges }) =>
|
|
156
|
+
textChanges.map(({ newText }) => newText),
|
|
157
|
+
),
|
|
158
|
+
).toContain(
|
|
159
|
+
`${indentationForExtractedConstant[indentationStyle]}const newLocal = "a" + value;\n`,
|
|
160
|
+
);
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("Yaml collection support", () => {
|
|
168
|
+
const requestFile = "/collection/request.yml";
|
|
169
|
+
const collectionFiles = {
|
|
170
|
+
"/collection/opencollection.yml": "opencollection: 1.0.0\n",
|
|
171
|
+
[requestFile]: `runtime:
|
|
172
|
+
scripts:
|
|
173
|
+
- type: tests
|
|
174
|
+
code: |-
|
|
175
|
+
valeu;
|
|
176
|
+
`,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
it("only provides diagnostics for scripts in Yaml files once Yaml collections are supported", () => {
|
|
180
|
+
const plugin = createPlugin({ yamlCollectionSupport: false });
|
|
181
|
+
const languageService = createLanguageServiceWithPlugin(
|
|
182
|
+
collectionFiles,
|
|
183
|
+
plugin,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(languageService.getSemanticDiagnostics(requestFile)).toEqual([]);
|
|
187
|
+
|
|
188
|
+
plugin.onConfigurationChanged({ yamlCollectionSupport: true });
|
|
189
|
+
|
|
190
|
+
expect(
|
|
191
|
+
languageService
|
|
192
|
+
.getSemanticDiagnostics(requestFile)
|
|
193
|
+
.map(({ code }) => code),
|
|
194
|
+
).toEqual([2_304]); // Cannot find name 'valeu'.
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("does not provide formatting edits for Yaml files", () => {
|
|
198
|
+
const languageService =
|
|
199
|
+
createLanguageServiceWithPlugin(collectionFiles);
|
|
200
|
+
|
|
201
|
+
expect(
|
|
202
|
+
languageService.getFormattingEditsForDocument(
|
|
203
|
+
requestFile,
|
|
204
|
+
ts.getDefaultFormatCodeSettings(),
|
|
205
|
+
),
|
|
206
|
+
).toEqual([]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
function createLanguageServiceWithPlugin(
|
|
211
|
+
files: Record<string, string>,
|
|
212
|
+
plugin = createPlugin({ yamlCollectionSupport: true }),
|
|
213
|
+
) {
|
|
214
|
+
const languageServiceHost: ts.LanguageServiceHost = {
|
|
215
|
+
getScriptFileNames: () =>
|
|
216
|
+
Object.keys(files).filter(
|
|
217
|
+
(fileName) => !fileName.endsWith(".json"),
|
|
218
|
+
),
|
|
219
|
+
getScriptVersion: () => "1",
|
|
220
|
+
getScriptSnapshot: (fileName) => {
|
|
221
|
+
const content = files[fileName] ?? ts.sys.readFile(fileName);
|
|
222
|
+
|
|
223
|
+
return content != undefined
|
|
224
|
+
? ts.ScriptSnapshot.fromString(content)
|
|
225
|
+
: undefined;
|
|
226
|
+
},
|
|
227
|
+
getCurrentDirectory: () => "/collection",
|
|
228
|
+
// Like for inferred projects of the Typescript server, files with any extension are handled.
|
|
229
|
+
getCompilationSettings: () => ({
|
|
230
|
+
allowJs: true,
|
|
231
|
+
allowNonTsExtensions: true,
|
|
232
|
+
target: ts.ScriptTarget.ES2020,
|
|
233
|
+
}),
|
|
234
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
235
|
+
fileExists: (fileName) =>
|
|
236
|
+
fileName in files || ts.sys.fileExists(fileName),
|
|
237
|
+
readFile: (fileName) => files[fileName] ?? ts.sys.readFile(fileName),
|
|
238
|
+
};
|
|
239
|
+
const serverHost = {
|
|
240
|
+
directoryExists: (directory: string) =>
|
|
241
|
+
Object.keys(files).some((fileName) =>
|
|
242
|
+
fileName.startsWith(`${directory}/`),
|
|
243
|
+
),
|
|
244
|
+
readDirectory: (directory: string) =>
|
|
245
|
+
Object.keys(files).filter(
|
|
246
|
+
(fileName) => dirname(fileName) == directory,
|
|
247
|
+
),
|
|
248
|
+
} as unknown as ts.server.ServerHost;
|
|
249
|
+
|
|
250
|
+
return plugin.create({
|
|
251
|
+
languageService: ts.createLanguageService(languageServiceHost),
|
|
252
|
+
languageServiceHost,
|
|
253
|
+
serverHost,
|
|
254
|
+
} as ts.server.PluginCreateInfo);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function createPlugin(config: PluginConfig) {
|
|
258
|
+
const plugin = init({ typescript: ts });
|
|
259
|
+
plugin.onConfigurationChanged(config);
|
|
260
|
+
|
|
261
|
+
return plugin;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getCodeFixNames(
|
|
265
|
+
languageService: ts.LanguageService,
|
|
266
|
+
fileName: string,
|
|
267
|
+
fileContent: string,
|
|
268
|
+
diagnosticText: string,
|
|
269
|
+
) {
|
|
270
|
+
const diagnostic = languageService
|
|
271
|
+
.getSemanticDiagnostics(fileName)
|
|
272
|
+
.find(
|
|
273
|
+
({ start, length }) =>
|
|
274
|
+
start != undefined &&
|
|
275
|
+
length != undefined &&
|
|
276
|
+
fileContent.substring(start, start + length) == diagnosticText,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (!diagnostic || diagnostic.start == undefined) {
|
|
280
|
+
throw new Error(`No diagnostic found for '${diagnosticText}'.`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return languageService
|
|
284
|
+
.getCodeFixesAtPosition(
|
|
285
|
+
fileName,
|
|
286
|
+
diagnostic.start,
|
|
287
|
+
diagnostic.start + (diagnostic.length ?? 0),
|
|
288
|
+
[diagnostic.code],
|
|
289
|
+
ts.getDefaultFormatCodeSettings(),
|
|
290
|
+
{},
|
|
291
|
+
)
|
|
292
|
+
.map(({ fixName }) => fixName);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getSelection(fileContent: string, selectedText: string) {
|
|
296
|
+
const start = fileContent.indexOf(selectedText);
|
|
297
|
+
|
|
298
|
+
return { pos: start, end: start + selectedText.length };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getEditsForRefactor(
|
|
302
|
+
languageService: ts.LanguageService,
|
|
303
|
+
fileName: string,
|
|
304
|
+
selection: ts.TextRange,
|
|
305
|
+
actionDescription: string,
|
|
306
|
+
formatOptions?: ts.FormatCodeSettings,
|
|
307
|
+
) {
|
|
308
|
+
const applicableRefactor = languageService
|
|
309
|
+
.getApplicableRefactors(fileName, selection, {})
|
|
310
|
+
.flatMap(({ name, actions }) =>
|
|
311
|
+
actions.map((action) => ({ refactorName: name, action })),
|
|
312
|
+
)
|
|
313
|
+
.find(({ action }) => action.description == actionDescription);
|
|
314
|
+
|
|
315
|
+
if (!applicableRefactor) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Refactoring '${actionDescription}' is not applicable.`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return languageService.getEditsForRefactor(
|
|
322
|
+
fileName,
|
|
323
|
+
{ ...ts.getDefaultFormatCodeSettings(), ...formatOptions },
|
|
324
|
+
selection,
|
|
325
|
+
applicableRefactor.refactorName,
|
|
326
|
+
applicableRefactor.action.name,
|
|
327
|
+
{},
|
|
328
|
+
);
|
|
329
|
+
}
|