@laurence79/wireit 0.14.13-shared-cache.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/LICENSE +202 -0
- package/README.md +1062 -0
- package/bin/wireit.js +9 -0
- package/lib/analyzer.js +1600 -0
- package/lib/caching/cache.js +7 -0
- package/lib/caching/github-actions-cache.js +832 -0
- package/lib/caching/local-cache.js +78 -0
- package/lib/caching/shared-cache.js +256 -0
- package/lib/cli-options.js +495 -0
- package/lib/cli.js +177 -0
- package/lib/config.js +18 -0
- package/lib/error.js +160 -0
- package/lib/event.js +7 -0
- package/lib/execution/base.js +108 -0
- package/lib/execution/no-command.js +32 -0
- package/lib/execution/service.js +1017 -0
- package/lib/execution/standard.js +683 -0
- package/lib/executor.js +249 -0
- package/lib/fingerprint.js +164 -0
- package/lib/ide.js +583 -0
- package/lib/language-server.js +135 -0
- package/lib/logging/combination-logger.js +41 -0
- package/lib/logging/debug-logger.js +43 -0
- package/lib/logging/logger.js +38 -0
- package/lib/logging/metrics-logger.js +108 -0
- package/lib/logging/quiet/run-tracker.js +597 -0
- package/lib/logging/quiet/stack-map.js +41 -0
- package/lib/logging/quiet/writeover-line.js +197 -0
- package/lib/logging/quiet-logger.js +78 -0
- package/lib/logging/simple-logger.js +296 -0
- package/lib/logging/watch-logger.js +81 -0
- package/lib/script-child-process.js +270 -0
- package/lib/util/ast.js +71 -0
- package/lib/util/async-cache.js +24 -0
- package/lib/util/copy.js +120 -0
- package/lib/util/deferred.js +35 -0
- package/lib/util/delete.js +120 -0
- package/lib/util/dispose.js +16 -0
- package/lib/util/fs.js +258 -0
- package/lib/util/glob.js +255 -0
- package/lib/util/line-monitor.js +69 -0
- package/lib/util/manifest.js +31 -0
- package/lib/util/optimize-mkdirs.js +55 -0
- package/lib/util/package-json-reader.js +61 -0
- package/lib/util/package-json.js +179 -0
- package/lib/util/script-data-dir.js +19 -0
- package/lib/util/shuffle.js +16 -0
- package/lib/util/unreachable.js +12 -0
- package/lib/util/windows.js +87 -0
- package/lib/util/worker-pool.js +61 -0
- package/lib/watcher.js +396 -0
- package/package.json +470 -0
- package/schema.json +132 -0
- package/wireit.svg +1 -0
package/lib/ide.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from './util/fs.js';
|
|
7
|
+
import { Analyzer } from './analyzer.js';
|
|
8
|
+
import * as url from 'url';
|
|
9
|
+
import * as pathlib from 'path';
|
|
10
|
+
import * as jsonParser from 'jsonc-parser';
|
|
11
|
+
import { offsetInsideRange, OffsetToPositionConverter, } from './error.js';
|
|
12
|
+
class OverlayFilesystem {
|
|
13
|
+
constructor() {
|
|
14
|
+
// filename to contents
|
|
15
|
+
this.overlay = new Map();
|
|
16
|
+
}
|
|
17
|
+
async readFile(path, options) {
|
|
18
|
+
const contents = this.overlay.get(path);
|
|
19
|
+
if (contents !== undefined) {
|
|
20
|
+
return contents;
|
|
21
|
+
}
|
|
22
|
+
return fs.readFile(path, options);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export const completionItemKinds = {
|
|
26
|
+
service: 24, // CompletionItemKind.Operator
|
|
27
|
+
normalScript: 2, // CompletionItemKind.Method
|
|
28
|
+
dependenciesOnly: 9, // CompletionItemKind.Module
|
|
29
|
+
filesOnly: 17, // CompletionItemKind.File
|
|
30
|
+
folder: 19, // CompletionItemKind.Folder
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The interface for an IDE to communicate with wireit's analysis pipeline.
|
|
34
|
+
*
|
|
35
|
+
* An IDE has certain files open with in-memory buffers. These buffers often
|
|
36
|
+
* shadow the files on disk, and in those cases we want to use the buffer if
|
|
37
|
+
* it's available, and fall back on disk contents if not.
|
|
38
|
+
*
|
|
39
|
+
* Generally the user only cares about the in-memory files, at least for
|
|
40
|
+
* most features like diagnostics.
|
|
41
|
+
*/
|
|
42
|
+
export class IdeAnalyzer {
|
|
43
|
+
#overlayFs;
|
|
44
|
+
#workspaceRoots = [];
|
|
45
|
+
#analyzer;
|
|
46
|
+
constructor() {
|
|
47
|
+
this.#overlayFs = new OverlayFilesystem();
|
|
48
|
+
this.#analyzer = new Analyzer('npm', undefined, this.#overlayFs);
|
|
49
|
+
}
|
|
50
|
+
setWorkspaceRoots(roots) {
|
|
51
|
+
this.#workspaceRoots = roots;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Adds the file to the set of open files if it wasn't already,
|
|
55
|
+
* and specifies its contents. Open files are defined by their
|
|
56
|
+
* in memory contents, not by their on-disk contents.
|
|
57
|
+
*
|
|
58
|
+
* We also only care about diagnostics for open files.
|
|
59
|
+
*
|
|
60
|
+
* IDEs will typically call this method when a user opens a package.json file
|
|
61
|
+
* for editing, as well as once for each edit the user makes.
|
|
62
|
+
*/
|
|
63
|
+
setOpenFileContents(path, contents) {
|
|
64
|
+
this.#overlayFs.overlay.set(path, contents);
|
|
65
|
+
this.#analyzer = new Analyzer('npm', undefined, this.#overlayFs);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Removes a file from the set of open files.
|
|
69
|
+
*/
|
|
70
|
+
closeFile(path) {
|
|
71
|
+
this.#overlayFs.overlay.delete(path);
|
|
72
|
+
this.#analyzer = new Analyzer('npm', undefined, this.#overlayFs);
|
|
73
|
+
}
|
|
74
|
+
get openFiles() {
|
|
75
|
+
return this.#overlayFs.overlay.keys();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Calculates and returns diagnostics for open files. If a file has no
|
|
79
|
+
* diagnostics then we don't include an entry for it at all.
|
|
80
|
+
*/
|
|
81
|
+
async getDiagnostics() {
|
|
82
|
+
const diagnostics = new Map();
|
|
83
|
+
function addDiagnostic(diagnostic) {
|
|
84
|
+
const path = diagnostic.location.file.path;
|
|
85
|
+
if (!openFiles.has(path)) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const converted = convertDiagnostic(diagnostic);
|
|
89
|
+
let set = diagnostics.get(path);
|
|
90
|
+
if (set === undefined) {
|
|
91
|
+
set = new Set();
|
|
92
|
+
diagnostics.set(path, set);
|
|
93
|
+
}
|
|
94
|
+
set.add(converted);
|
|
95
|
+
}
|
|
96
|
+
const openFiles = new Set(this.openFiles);
|
|
97
|
+
for (const failure of await this.#analyzer.analyzeFiles([...openFiles])) {
|
|
98
|
+
if (failure.diagnostic !== undefined) {
|
|
99
|
+
addDiagnostic(failure.diagnostic);
|
|
100
|
+
}
|
|
101
|
+
if (failure.diagnostics !== undefined) {
|
|
102
|
+
for (const diagnostic of failure.diagnostics) {
|
|
103
|
+
addDiagnostic(diagnostic);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return diagnostics;
|
|
108
|
+
}
|
|
109
|
+
async getCodeActions(path, range) {
|
|
110
|
+
const codeActions = [];
|
|
111
|
+
// file isn't open
|
|
112
|
+
if (!this.#overlayFs.overlay.has(path)) {
|
|
113
|
+
return codeActions;
|
|
114
|
+
}
|
|
115
|
+
const packageDir = pathlib.dirname(path);
|
|
116
|
+
// If there are any syntax-level errors for the file, we don't want to
|
|
117
|
+
// offer any code actions.
|
|
118
|
+
const packageJsonResult = await this.#analyzer.getPackageJson(packageDir);
|
|
119
|
+
if (!packageJsonResult.ok || packageJsonResult.value.failures.length > 0) {
|
|
120
|
+
return codeActions;
|
|
121
|
+
}
|
|
122
|
+
const packageJson = packageJsonResult.value;
|
|
123
|
+
const ourRange = OffsetToPositionConverter.get(packageJson.jsonFile).ideRangeToRange(range);
|
|
124
|
+
const scriptInfo = await this.#getInfoAboutLocation(packageJson, ourRange.offset);
|
|
125
|
+
if (scriptInfo === undefined) {
|
|
126
|
+
return codeActions;
|
|
127
|
+
}
|
|
128
|
+
if (scriptInfo.kind === 'dependency') {
|
|
129
|
+
// No code actions for dependencies yet.
|
|
130
|
+
return codeActions;
|
|
131
|
+
}
|
|
132
|
+
const { script, scriptSyntaxInfo: { name, scriptNode, wireitConfigNode }, } = scriptInfo;
|
|
133
|
+
if (scriptInfo.kind === 'scripts-section-script' &&
|
|
134
|
+
scriptNode !== undefined &&
|
|
135
|
+
wireitConfigNode === undefined) {
|
|
136
|
+
const edit = getEdit(packageJson.jsonFile, [
|
|
137
|
+
{ path: ['scripts', name], value: 'wireit' },
|
|
138
|
+
{
|
|
139
|
+
path: ['wireit', name],
|
|
140
|
+
value: { command: scriptNode.value },
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
codeActions.push({
|
|
144
|
+
title: `Refactor this script to use wireit.`,
|
|
145
|
+
kind: 'refactor.extract',
|
|
146
|
+
edit,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (scriptInfo.kind === 'wireit-section-script' &&
|
|
150
|
+
scriptNode === undefined) {
|
|
151
|
+
const edit = getEdit(packageJson.jsonFile, [
|
|
152
|
+
{ path: ['scripts', script.name], value: 'wireit' },
|
|
153
|
+
]);
|
|
154
|
+
codeActions.push({
|
|
155
|
+
title: `Add this script to the "scripts" section.`,
|
|
156
|
+
/**
|
|
157
|
+
* Quoting https://microsoft.github.io//language-server-protocol/specifications/lsp/3.17/specification/
|
|
158
|
+
*
|
|
159
|
+
* > 'Fix all' actions automatically fix errors that have a clear fix
|
|
160
|
+
* > that do not require user input. They should not suppress errors
|
|
161
|
+
* > or perform unsafe fixes such as generating new types or classes.
|
|
162
|
+
*/
|
|
163
|
+
kind: 'source.fixAll',
|
|
164
|
+
edit,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (scriptNode === undefined ||
|
|
168
|
+
wireitConfigNode === undefined ||
|
|
169
|
+
scriptNode.value === 'wireit') {
|
|
170
|
+
return codeActions;
|
|
171
|
+
}
|
|
172
|
+
// Ok, so there's definitely a wireit config and an entry in the scripts
|
|
173
|
+
// section, however the scripts section has its own command.
|
|
174
|
+
// In this case, we want to offer the user the option to move that command
|
|
175
|
+
// into the wireit section, but we need to be careful that we don't
|
|
176
|
+
// lose the user's command.
|
|
177
|
+
// Let's find the command, if any, in the wireit config.
|
|
178
|
+
let wireitCommand;
|
|
179
|
+
for (const propNode of wireitConfigNode.children ?? []) {
|
|
180
|
+
if (propNode.type !== 'property') {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const [key, value] = propNode.children ?? [];
|
|
184
|
+
if (key?.value !== 'command') {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (typeof value?.value !== 'string') {
|
|
188
|
+
return codeActions; // This is invalid, so we don't offer anything.
|
|
189
|
+
}
|
|
190
|
+
wireitCommand = value.value;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
if (wireitCommand === undefined) {
|
|
194
|
+
// this is the easy case, we can just move the command over
|
|
195
|
+
const edit = getEdit(packageJson.jsonFile, [
|
|
196
|
+
{ path: ['scripts', script.name], value: 'wireit' },
|
|
197
|
+
{ path: ['wireit', script.name, 'command'], value: scriptNode.value },
|
|
198
|
+
]);
|
|
199
|
+
codeActions.push({
|
|
200
|
+
title: `Move this script's command into the wireit config.`,
|
|
201
|
+
// This is mostly safe. The user might have moved the command
|
|
202
|
+
// back to the scripts section because they don't want some wireit
|
|
203
|
+
// features, like they don't want it to clean, or to run dependencies.
|
|
204
|
+
// So we don't want it to happen automatically, but it's very safe.
|
|
205
|
+
kind: 'quickfix',
|
|
206
|
+
isPreferred: true,
|
|
207
|
+
edit,
|
|
208
|
+
});
|
|
209
|
+
return codeActions;
|
|
210
|
+
}
|
|
211
|
+
// In the case where the commands are the same, we can just replace the
|
|
212
|
+
// script version with "wireit"
|
|
213
|
+
if (wireitCommand === scriptNode.value) {
|
|
214
|
+
const edit = getEdit(packageJson.jsonFile, [
|
|
215
|
+
{ path: ['scripts', script.name], value: 'wireit' },
|
|
216
|
+
]);
|
|
217
|
+
codeActions.push({
|
|
218
|
+
title: `Run "wireit" in the scripts section.`,
|
|
219
|
+
kind: 'quickfix',
|
|
220
|
+
isPreferred: true,
|
|
221
|
+
edit,
|
|
222
|
+
});
|
|
223
|
+
return codeActions;
|
|
224
|
+
}
|
|
225
|
+
// Ok here's the tricky part, we could lose user data.
|
|
226
|
+
const edit = getEdit(packageJson.jsonFile, [
|
|
227
|
+
{ path: ['scripts', script.name], value: 'wireit' },
|
|
228
|
+
{
|
|
229
|
+
path: ['wireit', script.name, '[the script command was]'],
|
|
230
|
+
value: scriptNode.value,
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
codeActions.push({
|
|
234
|
+
title: `Move this script's command into the wireit config.`,
|
|
235
|
+
kind: 'quickfix',
|
|
236
|
+
isPreferred: false,
|
|
237
|
+
edit,
|
|
238
|
+
});
|
|
239
|
+
return codeActions;
|
|
240
|
+
}
|
|
241
|
+
async getDefinition(path, position) {
|
|
242
|
+
const packageDir = pathlib.dirname(path);
|
|
243
|
+
const packageJsonResult = await this.#analyzer.getPackageJson(packageDir);
|
|
244
|
+
if (!packageJsonResult.ok) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
const packageJson = packageJsonResult.value;
|
|
248
|
+
const ourPosition = OffsetToPositionConverter.get(packageJson.jsonFile).idePositionToOffset(position);
|
|
249
|
+
const scriptInfo = await this.#getInfoAboutLocation(packageJson, ourPosition);
|
|
250
|
+
if (scriptInfo?.kind === 'dependency') {
|
|
251
|
+
const dep = scriptInfo.dependency;
|
|
252
|
+
const targetFile = dep.config.declaringFile;
|
|
253
|
+
const targetNode = dep.config.configAstNode ?? dep.config.scriptAstNode;
|
|
254
|
+
if (targetFile === undefined || targetNode === undefined) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const targetConverter = OffsetToPositionConverter.get(targetFile);
|
|
258
|
+
const sourceConverter = OffsetToPositionConverter.get(packageJson.jsonFile);
|
|
259
|
+
return [
|
|
260
|
+
{
|
|
261
|
+
originSelectionRange: sourceConverter.toIdeRange(scriptInfo.dependency.specifier),
|
|
262
|
+
targetUri: url.pathToFileURL(targetFile.path).toString(),
|
|
263
|
+
targetRange: targetConverter.toIdeRange(
|
|
264
|
+
// The parent is the property, including both key and value.
|
|
265
|
+
// So we preview the whole thing when looking at the definition:
|
|
266
|
+
// "build": {"command": "tsc"}
|
|
267
|
+
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
268
|
+
targetNode.parent ?? targetNode),
|
|
269
|
+
targetSelectionRange: targetConverter.toIdeRange(targetNode.name),
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
}
|
|
273
|
+
if (scriptInfo?.kind === 'scripts-section-script') {
|
|
274
|
+
const sourceConverter = OffsetToPositionConverter.get(packageJson.jsonFile);
|
|
275
|
+
const syntaxInfo = scriptInfo.scriptSyntaxInfo;
|
|
276
|
+
if (syntaxInfo.scriptNode && syntaxInfo.wireitConfigNode) {
|
|
277
|
+
// we can jump from the script section to the wireit config
|
|
278
|
+
return [
|
|
279
|
+
{
|
|
280
|
+
originSelectionRange: sourceConverter.toIdeRange(syntaxInfo.scriptNode.parent ?? syntaxInfo.scriptNode),
|
|
281
|
+
targetUri: url.pathToFileURL(packageJson.jsonFile.path).toString(),
|
|
282
|
+
targetRange: sourceConverter.toIdeRange(syntaxInfo.wireitConfigNode.parent ?? syntaxInfo.wireitConfigNode),
|
|
283
|
+
targetSelectionRange: sourceConverter.toIdeRange(syntaxInfo.wireitConfigNode.name),
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
async findAllReferences(path, position) {
|
|
290
|
+
const packageDir = pathlib.dirname(path);
|
|
291
|
+
const packageJsonResult = await this.#analyzer.getPackageJson(packageDir);
|
|
292
|
+
if (!packageJsonResult.ok) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
const packageJson = packageJsonResult.value;
|
|
296
|
+
const ourPosition = OffsetToPositionConverter.get(packageJson.jsonFile).idePositionToOffset(position);
|
|
297
|
+
const scriptInfo = await this.#getInfoAboutLocation(packageJson, ourPosition);
|
|
298
|
+
if (scriptInfo == null) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
let scriptToLookup;
|
|
302
|
+
if (scriptInfo.kind === 'dependency') {
|
|
303
|
+
scriptToLookup = scriptInfo.dependency.config;
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
scriptToLookup = scriptInfo.script;
|
|
307
|
+
}
|
|
308
|
+
const allScripts = await this.#analyzer.analyzeAllScripts([
|
|
309
|
+
...this.openFiles,
|
|
310
|
+
...this.#workspaceRoots.map((r) => pathlib.join(r, 'package.json')),
|
|
311
|
+
]);
|
|
312
|
+
const references = [];
|
|
313
|
+
for (const script of allScripts) {
|
|
314
|
+
const dependencies = script.placeholder.dependencies;
|
|
315
|
+
if (dependencies == null) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
for (const dep of dependencies) {
|
|
319
|
+
const depFile = script.placeholder.declaringFile;
|
|
320
|
+
if (depFile === undefined) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (dep.config.name !== scriptToLookup.name) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (dep.config.packageDir !== scriptToLookup.packageDir) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
references.push({
|
|
330
|
+
uri: url.pathToFileURL(depFile.path).toString(),
|
|
331
|
+
range: OffsetToPositionConverter.get(depFile).toIdeRange(dep.specifier),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// sort the references, first by file, then by offset
|
|
336
|
+
references.sort((a, b) => {
|
|
337
|
+
if (a.uri < b.uri) {
|
|
338
|
+
return -1;
|
|
339
|
+
}
|
|
340
|
+
if (a.uri > b.uri) {
|
|
341
|
+
return 1;
|
|
342
|
+
}
|
|
343
|
+
return a.range.start.line - b.range.start.line;
|
|
344
|
+
});
|
|
345
|
+
return references;
|
|
346
|
+
}
|
|
347
|
+
async getCompletions(path, position) {
|
|
348
|
+
const packageDir = pathlib.dirname(path);
|
|
349
|
+
const packageJsonResult = await this.#analyzer.getPackageJson(packageDir);
|
|
350
|
+
if (!packageJsonResult.ok) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
const packageJson = packageJsonResult.value;
|
|
354
|
+
const ourPosition = OffsetToPositionConverter.get(packageJson.jsonFile).idePositionToOffset(position);
|
|
355
|
+
const scriptInfo = await this.#getInfoAboutLocation(packageJson, ourPosition);
|
|
356
|
+
if (scriptInfo === undefined) {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
let scriptSpecifier;
|
|
360
|
+
if (scriptInfo.kind !== 'dependency') {
|
|
361
|
+
// Annoyingly, it's particularly important that we offer completions
|
|
362
|
+
// when the user is typing inside an empty dependency specifier, which
|
|
363
|
+
// is not a syntactically valid script config. So we need special logic
|
|
364
|
+
// here
|
|
365
|
+
const dependenciesProp = scriptInfo.scriptSyntaxInfo.wireitConfigNode?.children?.find((child) => child.type === 'property' &&
|
|
366
|
+
child.children?.[0]?.value === 'dependencies');
|
|
367
|
+
const dependency = dependenciesProp?.children?.[1]?.children?.find((child) => offsetInsideRange(ourPosition, child));
|
|
368
|
+
if (typeof dependency?.value !== 'string') {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
scriptSpecifier = dependency;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
scriptSpecifier = scriptInfo.dependency.specifier;
|
|
375
|
+
}
|
|
376
|
+
// Ok, the user is typing inside a dependency specifier, so we want to
|
|
377
|
+
// offer them completion items. Next question, are we in the (optional)
|
|
378
|
+
// file path portion of the specifier, or the script name portion?
|
|
379
|
+
const distanceInto = ourPosition - scriptSpecifier.offset - 1; /* for the leading quote */
|
|
380
|
+
const specifierBeforeCursor = scriptSpecifier.value.slice(0, distanceInto);
|
|
381
|
+
let targetPackageJson;
|
|
382
|
+
let targetPackageDir;
|
|
383
|
+
if (specifierBeforeCursor.startsWith('.')) {
|
|
384
|
+
const indexOfColon = specifierBeforeCursor.indexOf(':');
|
|
385
|
+
if (indexOfColon === -1) {
|
|
386
|
+
// We'd be autocompleting on the file path portion of the specifier.
|
|
387
|
+
// Not implemented yet.
|
|
388
|
+
const items = await this.#completionItemsForPath(packageDir, specifierBeforeCursor);
|
|
389
|
+
if (items == null) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
isIncomplete: true,
|
|
394
|
+
items,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
targetPackageDir = pathlib.join(packageDir, specifierBeforeCursor.slice(0, indexOfColon));
|
|
398
|
+
const result = await this.#analyzer.getPackageJson(targetPackageDir);
|
|
399
|
+
if (!result.ok) {
|
|
400
|
+
return undefined;
|
|
401
|
+
}
|
|
402
|
+
targetPackageJson = result.value;
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
targetPackageJson = packageJson;
|
|
406
|
+
targetPackageDir = packageDir;
|
|
407
|
+
}
|
|
408
|
+
// analyze the scripts in this file
|
|
409
|
+
const potentiallyValidScripts = await Promise.all([...targetPackageJson.scripts].map((script) => {
|
|
410
|
+
return this.#analyzer.analyzeIgnoringErrors({
|
|
411
|
+
name: script.name,
|
|
412
|
+
packageDir: targetPackageDir,
|
|
413
|
+
});
|
|
414
|
+
}));
|
|
415
|
+
const result = {
|
|
416
|
+
// If the user hasn't typed anything yet, our results are incomplete
|
|
417
|
+
// because they could type ./ or ../ and we don't complete those yet.
|
|
418
|
+
isIncomplete: specifierBeforeCursor === '',
|
|
419
|
+
items: potentiallyValidScripts.map((script) => {
|
|
420
|
+
return {
|
|
421
|
+
label: script.name,
|
|
422
|
+
kind: this.#completionKindForScript(script),
|
|
423
|
+
};
|
|
424
|
+
}),
|
|
425
|
+
};
|
|
426
|
+
// Sort results for deterministic tests.
|
|
427
|
+
result.items.sort((a, b) => a.label.localeCompare(b.label));
|
|
428
|
+
return result;
|
|
429
|
+
}
|
|
430
|
+
#completionKindForScript(script) {
|
|
431
|
+
if (script !== undefined) {
|
|
432
|
+
if (script.service !== undefined) {
|
|
433
|
+
return completionItemKinds.service;
|
|
434
|
+
}
|
|
435
|
+
else if (script.command !== undefined) {
|
|
436
|
+
return completionItemKinds.normalScript;
|
|
437
|
+
}
|
|
438
|
+
else if (script.dependencies !== undefined &&
|
|
439
|
+
script.dependencies.length > 0) {
|
|
440
|
+
return completionItemKinds.dependenciesOnly;
|
|
441
|
+
}
|
|
442
|
+
else if (script.files !== undefined) {
|
|
443
|
+
return completionItemKinds.filesOnly;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return completionItemKinds.normalScript;
|
|
447
|
+
}
|
|
448
|
+
async #completionItemsForPath(packageDir, specifierSoFar) {
|
|
449
|
+
const result = [];
|
|
450
|
+
const relPathToDirCompletingIn = specifierSoFar.endsWith('/')
|
|
451
|
+
? specifierSoFar
|
|
452
|
+
: pathlib.dirname(specifierSoFar);
|
|
453
|
+
const pathToDirCompletingIn = pathlib.join(packageDir, relPathToDirCompletingIn);
|
|
454
|
+
let dirContents;
|
|
455
|
+
try {
|
|
456
|
+
dirContents = await fs.readdir(pathToDirCompletingIn, {
|
|
457
|
+
withFileTypes: true,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
for (const dirent of dirContents) {
|
|
464
|
+
if (dirent.name === 'node_modules' || dirent.name.startsWith('.')) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (dirent.isDirectory()) {
|
|
468
|
+
result.push({
|
|
469
|
+
label: dirent.name,
|
|
470
|
+
kind: completionItemKinds.folder,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
async getPackageJsonForTest(filename) {
|
|
477
|
+
const packageDir = pathlib.dirname(filename);
|
|
478
|
+
const packageJsonResult = await this.#analyzer.getPackageJson(packageDir);
|
|
479
|
+
if (!packageJsonResult.ok) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
return packageJsonResult.value;
|
|
483
|
+
}
|
|
484
|
+
async #getInfoAboutLocation(packageJson, offset) {
|
|
485
|
+
const locationInfo = packageJson.getInfoAboutLocation(offset);
|
|
486
|
+
if (locationInfo === undefined) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const script = await this.#analyzer.analyzeIgnoringErrors({
|
|
490
|
+
name: locationInfo.scriptSyntaxInfo.name,
|
|
491
|
+
packageDir: pathlib.dirname(packageJson.jsonFile.path),
|
|
492
|
+
});
|
|
493
|
+
for (const dep of script.dependencies ?? []) {
|
|
494
|
+
if (offsetInsideRange(offset, dep.specifier)) {
|
|
495
|
+
return {
|
|
496
|
+
kind: 'dependency',
|
|
497
|
+
dependency: dep,
|
|
498
|
+
script: script,
|
|
499
|
+
scriptSyntax: locationInfo.scriptSyntaxInfo,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
...locationInfo,
|
|
505
|
+
script,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
function getEdit(file, modifications) {
|
|
510
|
+
const edits = [];
|
|
511
|
+
for (const { path, value } of modifications) {
|
|
512
|
+
edits.push(...jsonParser.modify(file.contents, path, value, inferModificationOptions(file)));
|
|
513
|
+
}
|
|
514
|
+
const converter = OffsetToPositionConverter.get(file);
|
|
515
|
+
const textEdits = edits.map((e) => {
|
|
516
|
+
return {
|
|
517
|
+
range: converter.toIdeRange(e),
|
|
518
|
+
newText: e.content,
|
|
519
|
+
};
|
|
520
|
+
});
|
|
521
|
+
return { changes: { [file.path]: textEdits } };
|
|
522
|
+
}
|
|
523
|
+
function inferModificationOptions(file) {
|
|
524
|
+
const firstPostNewlineWhitespace = file.contents.match(/\n(\s+)/)?.[1];
|
|
525
|
+
if (firstPostNewlineWhitespace === undefined) {
|
|
526
|
+
return {};
|
|
527
|
+
}
|
|
528
|
+
if (/^ +$/.test(firstPostNewlineWhitespace)) {
|
|
529
|
+
return {
|
|
530
|
+
formattingOptions: {
|
|
531
|
+
insertSpaces: true,
|
|
532
|
+
tabSize: firstPostNewlineWhitespace.length,
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
else if (/^\t+$/.test(firstPostNewlineWhitespace)) {
|
|
537
|
+
return {
|
|
538
|
+
formattingOptions: {
|
|
539
|
+
insertSpaces: false,
|
|
540
|
+
tabSize: firstPostNewlineWhitespace.length,
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
return {};
|
|
545
|
+
}
|
|
546
|
+
function convertDiagnostic(d) {
|
|
547
|
+
const converter = OffsetToPositionConverter.get(d.location.file);
|
|
548
|
+
let relatedInformation;
|
|
549
|
+
if (d.supplementalLocations) {
|
|
550
|
+
relatedInformation = [];
|
|
551
|
+
for (const loc of d.supplementalLocations) {
|
|
552
|
+
relatedInformation.push({
|
|
553
|
+
location: {
|
|
554
|
+
uri: url.pathToFileURL(loc.location.file.path).toString(),
|
|
555
|
+
range: converter.toIdeRange(loc.location.range),
|
|
556
|
+
},
|
|
557
|
+
message: loc.message,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
severity: convertSeverity(d.severity),
|
|
563
|
+
message: d.message,
|
|
564
|
+
source: 'wireit',
|
|
565
|
+
range: converter.toIdeRange(d.location.range),
|
|
566
|
+
relatedInformation,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function convertSeverity(severity) {
|
|
570
|
+
switch (severity) {
|
|
571
|
+
case 'error':
|
|
572
|
+
return 1; // DiagnosticSeverity.Error;
|
|
573
|
+
case 'warning':
|
|
574
|
+
return 2; // DiagnosticSeverity.Warning;
|
|
575
|
+
case 'info':
|
|
576
|
+
return 3; // DiagnosticSeverity.Information;
|
|
577
|
+
default: {
|
|
578
|
+
const never = severity;
|
|
579
|
+
throw new Error(`Unexpected severity: ${String(never)}`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
//# sourceMappingURL=ide.js.map
|