@astrojs/language-server 0.15.0 → 0.17.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/CHANGELOG.md +32 -0
- package/dist/check.js +1 -2
- package/dist/core/config/ConfigManager.d.ts +20 -16
- package/dist/core/config/ConfigManager.js +112 -46
- package/dist/core/config/interfaces.d.ts +0 -52
- package/dist/core/documents/AstroDocument.d.ts +1 -0
- package/dist/core/documents/AstroDocument.js +1 -0
- package/dist/core/documents/DocumentMapper.d.ts +2 -0
- package/dist/core/documents/DocumentMapper.js +9 -9
- package/dist/core/documents/parseAstro.js +1 -1
- package/dist/core/documents/utils.d.ts +1 -0
- package/dist/core/documents/utils.js +19 -5
- package/dist/plugins/PluginHost.d.ts +2 -1
- package/dist/plugins/PluginHost.js +8 -6
- package/dist/plugins/astro/AstroPlugin.d.ts +1 -6
- package/dist/plugins/astro/AstroPlugin.js +1 -83
- package/dist/plugins/astro/features/CompletionsProvider.d.ts +4 -5
- package/dist/plugins/astro/features/CompletionsProvider.js +49 -59
- package/dist/plugins/css/CSSPlugin.d.ts +5 -5
- package/dist/plugins/css/CSSPlugin.js +41 -20
- package/dist/plugins/html/HTMLPlugin.d.ts +4 -4
- package/dist/plugins/html/HTMLPlugin.js +20 -16
- package/dist/plugins/html/features/astro-attributes.js +44 -27
- package/dist/plugins/typescript/LanguageServiceManager.js +1 -1
- package/dist/plugins/typescript/TypeScriptPlugin.d.ts +5 -4
- package/dist/plugins/typescript/TypeScriptPlugin.js +30 -108
- package/dist/plugins/typescript/astro-sys.js +3 -5
- package/dist/plugins/typescript/astro2tsx.js +1 -2
- package/dist/plugins/typescript/features/CodeActionsProvider.d.ts +16 -0
- package/dist/plugins/typescript/features/CodeActionsProvider.js +206 -0
- package/dist/plugins/typescript/features/CompletionsProvider.d.ts +5 -2
- package/dist/plugins/typescript/features/CompletionsProvider.js +116 -68
- package/dist/plugins/typescript/features/DefinitionsProvider.d.ts +9 -0
- package/dist/plugins/typescript/features/DefinitionsProvider.js +57 -0
- package/dist/plugins/typescript/features/DiagnosticsProvider.js +60 -18
- package/dist/plugins/typescript/features/DocumentSymbolsProvider.js +3 -4
- package/dist/plugins/typescript/features/FoldingRangesProvider.js +13 -6
- package/dist/plugins/typescript/features/HoverProvider.js +14 -1
- package/dist/plugins/typescript/features/SemanticTokenProvider.js +1 -1
- package/dist/plugins/typescript/features/SignatureHelpProvider.js +11 -3
- package/dist/plugins/typescript/features/utils.d.ts +2 -0
- package/dist/plugins/typescript/features/utils.js +19 -3
- package/dist/plugins/typescript/language-service.js +23 -6
- package/dist/plugins/typescript/module-loader.js +1 -1
- package/dist/plugins/typescript/previewer.js +1 -1
- package/dist/plugins/typescript/snapshots/DocumentSnapshot.d.ts +22 -2
- package/dist/plugins/typescript/snapshots/DocumentSnapshot.js +48 -1
- package/dist/plugins/typescript/snapshots/SnapshotManager.js +2 -1
- package/dist/plugins/typescript/snapshots/utils.js +3 -6
- package/dist/plugins/typescript/utils.d.ts +12 -1
- package/dist/plugins/typescript/utils.js +29 -1
- package/dist/server.js +43 -14
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +16 -3
- package/package.json +2 -2
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CodeActionsProviderImpl = exports.sortImportKind = void 0;
|
|
7
|
+
const lodash_1 = require("lodash");
|
|
8
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
9
|
+
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
|
|
10
|
+
const documents_1 = require("../../../core/documents");
|
|
11
|
+
const utils_1 = require("../../../utils");
|
|
12
|
+
const utils_2 = require("../utils");
|
|
13
|
+
const CompletionsProvider_1 = require("./CompletionsProvider");
|
|
14
|
+
const utils_3 = require("./utils");
|
|
15
|
+
// These are VS Code specific CodeActionKind so they're not in the language server protocol
|
|
16
|
+
exports.sortImportKind = `${vscode_languageserver_types_1.CodeActionKind.Source}.sortImports`;
|
|
17
|
+
class CodeActionsProviderImpl {
|
|
18
|
+
constructor(languageServiceManager, configManager) {
|
|
19
|
+
this.languageServiceManager = languageServiceManager;
|
|
20
|
+
this.configManager = configManager;
|
|
21
|
+
}
|
|
22
|
+
async getCodeActions(document, range, context, cancellationToken) {
|
|
23
|
+
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
|
|
24
|
+
const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
|
|
25
|
+
const fragment = await tsDoc.createFragment();
|
|
26
|
+
const tsPreferences = await this.configManager.getTSPreferences(document);
|
|
27
|
+
const formatOptions = await this.configManager.getTSFormatConfig(document);
|
|
28
|
+
let result = [];
|
|
29
|
+
if (cancellationToken?.isCancellationRequested) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
if (context.only?.[0] === vscode_languageserver_types_1.CodeActionKind.SourceOrganizeImports) {
|
|
33
|
+
return await this.organizeSortImports(document, false, cancellationToken);
|
|
34
|
+
}
|
|
35
|
+
// The difference between Sort Imports and Organize Imports is that Sort Imports won't do anything destructive.
|
|
36
|
+
// For example, it won't remove unused imports whereas Organize Imports will
|
|
37
|
+
if (context.only?.[0] === exports.sortImportKind) {
|
|
38
|
+
return await this.organizeSortImports(document, true, cancellationToken);
|
|
39
|
+
}
|
|
40
|
+
if (context.only?.[0] === vscode_languageserver_types_1.CodeActionKind.Source) {
|
|
41
|
+
return [
|
|
42
|
+
...(await this.organizeSortImports(document, true, cancellationToken)),
|
|
43
|
+
...(await this.organizeSortImports(document, false, cancellationToken)),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
if (context.diagnostics.length && (!context.only || context.only.includes(vscode_languageserver_types_1.CodeActionKind.QuickFix))) {
|
|
47
|
+
const errorCodes = context.diagnostics
|
|
48
|
+
.map((diag) => Number(diag.code))
|
|
49
|
+
// We currently cannot support quick fix for unreachable code properly due to the way our TSX output is structured
|
|
50
|
+
.filter((code) => code !== 7027);
|
|
51
|
+
const html = document.html;
|
|
52
|
+
const node = html.findNodeAt(document.offsetAt(range.start));
|
|
53
|
+
let codeFixes;
|
|
54
|
+
let isInsideScript = false;
|
|
55
|
+
if (node.tag === 'script') {
|
|
56
|
+
const { snapshot: scriptTagSnapshot, filePath: scriptFilePath } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node);
|
|
57
|
+
const start = scriptTagSnapshot.offsetAt(scriptTagSnapshot.getGeneratedPosition(range.start));
|
|
58
|
+
const end = scriptTagSnapshot.offsetAt(scriptTagSnapshot.getGeneratedPosition(range.end));
|
|
59
|
+
codeFixes = lang.getCodeFixesAtPosition(scriptFilePath, start, end, errorCodes, formatOptions, tsPreferences);
|
|
60
|
+
codeFixes = codeFixes.map((fix) => ({
|
|
61
|
+
...fix,
|
|
62
|
+
changes: mapScriptTagFixToOriginal(fix.changes, scriptTagSnapshot),
|
|
63
|
+
}));
|
|
64
|
+
isInsideScript = true;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
|
|
68
|
+
const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
|
|
69
|
+
codeFixes = errorCodes.includes(2304)
|
|
70
|
+
? this.getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences)
|
|
71
|
+
: undefined;
|
|
72
|
+
codeFixes =
|
|
73
|
+
codeFixes ?? lang.getCodeFixesAtPosition(filePath, start, end, errorCodes, formatOptions, tsPreferences);
|
|
74
|
+
}
|
|
75
|
+
const codeActions = codeFixes.map((fix) => codeFixToCodeAction(fix, context.diagnostics, context.only ? vscode_languageserver_types_1.CodeActionKind.QuickFix : vscode_languageserver_types_1.CodeActionKind.Empty, isInsideScript));
|
|
76
|
+
result.push(...codeActions);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
function codeFixToCodeAction(codeFix, diagnostics, kind, isInsideScript) {
|
|
80
|
+
const documentChanges = codeFix.changes.map((change) => {
|
|
81
|
+
return vscode_languageserver_types_1.TextDocumentEdit.create(vscode_languageserver_types_1.OptionalVersionedTextDocumentIdentifier.create(document.getURL(), null), change.textChanges.map((edit) => {
|
|
82
|
+
let originalRange = (0, documents_1.mapRangeToOriginal)(fragment, (0, utils_2.convertRange)(fragment, edit.span));
|
|
83
|
+
// Inside scripts, we don't need to restrain the insertion of code inside a specific zone as it will be
|
|
84
|
+
// restricted to the area of the script tag by default
|
|
85
|
+
if (!isInsideScript) {
|
|
86
|
+
if (codeFix.fixName === 'import') {
|
|
87
|
+
return (0, CompletionsProvider_1.codeActionChangeToTextEdit)(document, fragment, false, edit);
|
|
88
|
+
}
|
|
89
|
+
if (codeFix.fixName === 'fixMissingFunctionDeclaration') {
|
|
90
|
+
originalRange = (0, utils_2.checkEndOfFileCodeInsert)(originalRange, document);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Make sure new imports are not added on the file line of the script tag
|
|
95
|
+
if (codeFix.fixName === 'import') {
|
|
96
|
+
const existingLine = (0, documents_1.getLineAtPosition)(document.positionAt(edit.span.start), document.getText());
|
|
97
|
+
const isNewImport = !existingLine.trim().startsWith('import');
|
|
98
|
+
if (!(edit.newText.startsWith('\n') || edit.newText.startsWith('\r\n')) && isNewImport) {
|
|
99
|
+
edit.newText = typescript_1.default.sys.newLine + edit.newText;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return vscode_languageserver_types_1.TextEdit.replace(originalRange, edit.newText);
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
const codeAction = vscode_languageserver_types_1.CodeAction.create(codeFix.description, {
|
|
107
|
+
documentChanges,
|
|
108
|
+
}, kind);
|
|
109
|
+
codeAction.diagnostics = diagnostics;
|
|
110
|
+
return codeAction;
|
|
111
|
+
}
|
|
112
|
+
function mapScriptTagFixToOriginal(changes, scriptTagSnapshot) {
|
|
113
|
+
return changes.map((change) => {
|
|
114
|
+
change.textChanges.map((edit) => {
|
|
115
|
+
edit.span.start = fragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(edit.span.start)));
|
|
116
|
+
return edit;
|
|
117
|
+
});
|
|
118
|
+
return change;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences) {
|
|
123
|
+
const sourceFile = lang.getProgram()?.getSourceFile(filePath);
|
|
124
|
+
if (!sourceFile) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const node = (0, utils_3.findContainingNode)(sourceFile, {
|
|
128
|
+
start,
|
|
129
|
+
length: end - start,
|
|
130
|
+
}, (n) => typescript_1.default.isJsxClosingElement(n) || typescript_1.default.isJsxOpeningLikeElement(n));
|
|
131
|
+
if (!node) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const tagName = node.tagName;
|
|
135
|
+
// Unlike quick fixes, completions will be able to find the component, so let's use those to get it
|
|
136
|
+
const completion = lang.getCompletionsAtPosition(filePath, tagName.getEnd(), tsPreferences, formatOptions);
|
|
137
|
+
if (!completion) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const name = tagName.getText();
|
|
141
|
+
const suffixedName = name + '__AstroComponent_';
|
|
142
|
+
const toFix = (c) => lang.getCompletionEntryDetails(filePath, end, c.name, {}, c.source, {}, c.data)?.codeActions?.map((a) => ({
|
|
143
|
+
...a,
|
|
144
|
+
description: (0, utils_2.removeAstroComponentSuffix)(a.description),
|
|
145
|
+
fixName: 'import',
|
|
146
|
+
})) ?? [];
|
|
147
|
+
return (0, lodash_1.flatten)(completion.entries.filter((c) => c.name === name || c.name === suffixedName).map(toFix));
|
|
148
|
+
}
|
|
149
|
+
async organizeSortImports(document, skipDestructiveCodeActions = false, cancellationToken) {
|
|
150
|
+
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
|
|
151
|
+
const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
|
|
152
|
+
const fragment = await tsDoc.createFragment();
|
|
153
|
+
if (cancellationToken?.isCancellationRequested) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
let changes = [];
|
|
157
|
+
if (document.astroMeta.frontmatter.state === 'closed') {
|
|
158
|
+
changes.push(...lang.organizeImports({ fileName: filePath, type: 'file', skipDestructiveCodeActions }, {}, {}));
|
|
159
|
+
}
|
|
160
|
+
document.scriptTags.forEach((scriptTag) => {
|
|
161
|
+
const { filePath: scriptFilePath, snapshot: scriptTagSnapshot } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
|
|
162
|
+
const edits = lang.organizeImports({ fileName: scriptFilePath, type: 'file', skipDestructiveCodeActions }, {}, {});
|
|
163
|
+
edits.forEach((edit) => {
|
|
164
|
+
edit.fileName = tsDoc.filePath;
|
|
165
|
+
edit.textChanges = edit.textChanges
|
|
166
|
+
.map((change) => {
|
|
167
|
+
change.span.start = fragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(change.span.start)));
|
|
168
|
+
return change;
|
|
169
|
+
})
|
|
170
|
+
// Since our last line is a (virtual) export, organize imports will try to rewrite it, so let's only take
|
|
171
|
+
// changes that actually happens inside the script tag
|
|
172
|
+
.filter((change) => {
|
|
173
|
+
return scriptTagSnapshot.isInGenerated(document.positionAt(change.span.start));
|
|
174
|
+
});
|
|
175
|
+
return edit;
|
|
176
|
+
});
|
|
177
|
+
changes.push(...edits);
|
|
178
|
+
});
|
|
179
|
+
const documentChanges = changes.map((change) => {
|
|
180
|
+
return vscode_languageserver_types_1.TextDocumentEdit.create(vscode_languageserver_types_1.OptionalVersionedTextDocumentIdentifier.create(document.url, null), change.textChanges.map((edit) => {
|
|
181
|
+
const range = (0, documents_1.mapRangeToOriginal)(fragment, (0, utils_2.convertRange)(fragment, edit.span));
|
|
182
|
+
return vscode_languageserver_types_1.TextEdit.replace(range, this.fixIndentationOfImports(edit.newText, range, document));
|
|
183
|
+
}));
|
|
184
|
+
});
|
|
185
|
+
return [
|
|
186
|
+
vscode_languageserver_types_1.CodeAction.create(skipDestructiveCodeActions ? 'Sort Imports' : 'Organize Imports', {
|
|
187
|
+
documentChanges,
|
|
188
|
+
}, skipDestructiveCodeActions ? exports.sortImportKind : vscode_languageserver_types_1.CodeActionKind.SourceOrganizeImports),
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
// "Organize Imports" will have edits that delete all imports by return empty edits
|
|
192
|
+
// and one edit which contains all the organized imports. Fix indentation
|
|
193
|
+
// of that one by prepending all lines with the indentation of the first line.
|
|
194
|
+
fixIndentationOfImports(edit, range, document) {
|
|
195
|
+
if (!edit || range.start.character === 0) {
|
|
196
|
+
return edit;
|
|
197
|
+
}
|
|
198
|
+
const existingLine = (0, documents_1.getLineAtPosition)(range.start, document.getText());
|
|
199
|
+
const leadingChars = existingLine.substring(0, range.start.character);
|
|
200
|
+
if (leadingChars.trim() !== '') {
|
|
201
|
+
return edit;
|
|
202
|
+
}
|
|
203
|
+
return (0, utils_1.modifyLines)(edit, (line, idx) => (idx === 0 || !line ? line : leadingChars + line));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
exports.CodeActionsProviderImpl = CodeActionsProviderImpl;
|
|
@@ -4,14 +4,17 @@ import { AstroDocument } from '../../../core/documents';
|
|
|
4
4
|
import ts from 'typescript';
|
|
5
5
|
import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
|
|
6
6
|
import { AstroSnapshotFragment } from '../snapshots/DocumentSnapshot';
|
|
7
|
+
import { ConfigManager } from '../../../core/config';
|
|
7
8
|
export interface CompletionItemData extends TextDocumentIdentifier {
|
|
8
9
|
filePath: string;
|
|
9
10
|
offset: number;
|
|
11
|
+
scriptTagIndex: number | undefined;
|
|
10
12
|
originalItem: ts.CompletionEntry;
|
|
11
13
|
}
|
|
12
14
|
export declare class CompletionsProviderImpl implements CompletionsProvider<CompletionItemData> {
|
|
13
15
|
private languageServiceManager;
|
|
14
|
-
|
|
16
|
+
private configManager;
|
|
17
|
+
constructor(languageServiceManager: LanguageServiceManager, configManager: ConfigManager);
|
|
15
18
|
private readonly validTriggerCharacters;
|
|
16
19
|
private isValidTriggerCharacter;
|
|
17
20
|
private lastCompletion?;
|
|
@@ -19,7 +22,6 @@ export declare class CompletionsProviderImpl implements CompletionsProvider<Comp
|
|
|
19
22
|
resolveCompletion(document: AstroDocument, item: AppCompletionItem<CompletionItemData>, cancellationToken?: CancellationToken): Promise<AppCompletionItem<CompletionItemData>>;
|
|
20
23
|
private toCompletionItem;
|
|
21
24
|
private isValidCompletion;
|
|
22
|
-
codeActionChangeToTextEdit(document: AstroDocument, fragment: AstroSnapshotFragment, change: ts.TextChange): TextEdit;
|
|
23
25
|
private getCompletionDocument;
|
|
24
26
|
/**
|
|
25
27
|
* If the textEdit is out of the word range of the triggered position
|
|
@@ -31,3 +33,4 @@ export declare class CompletionsProviderImpl implements CompletionsProvider<Comp
|
|
|
31
33
|
private getExistingImports;
|
|
32
34
|
private isAstroComponentImport;
|
|
33
35
|
}
|
|
36
|
+
export declare function codeActionChangeToTextEdit(document: AstroDocument, fragment: AstroSnapshotFragment, isInsideScriptTag: boolean, change: ts.TextChange): TextEdit;
|
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.CompletionsProviderImpl = void 0;
|
|
26
|
+
exports.codeActionChangeToTextEdit = exports.CompletionsProviderImpl = void 0;
|
|
27
27
|
const vscode_languageserver_1 = require("vscode-languageserver");
|
|
28
28
|
const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
|
|
29
29
|
const utils_1 = require("../../../core/documents/utils");
|
|
@@ -35,33 +35,24 @@ const utils_3 = require("../../../utils");
|
|
|
35
35
|
const lodash_1 = require("lodash");
|
|
36
36
|
const previewer_1 = require("../previewer");
|
|
37
37
|
const utils_4 = require("./utils");
|
|
38
|
-
const completionOptions = {
|
|
39
|
-
importModuleSpecifierPreference: 'relative',
|
|
40
|
-
importModuleSpecifierEnding: 'auto',
|
|
41
|
-
quotePreference: 'single',
|
|
42
|
-
includeCompletionsForModuleExports: true,
|
|
43
|
-
includeCompletionsForImportStatements: true,
|
|
44
|
-
includeCompletionsWithInsertText: true,
|
|
45
|
-
allowIncompleteCompletions: true,
|
|
46
|
-
includeCompletionsWithSnippetText: true,
|
|
47
|
-
};
|
|
48
38
|
// `import {...} from '..'` or `import ... from '..'`
|
|
49
39
|
// Note: Does not take into account if import is within a comment.
|
|
50
40
|
const scriptImportRegex = /\bimport\s+{([^}]*?)}\s+?from\s+['"`].+?['"`]|\bimport\s+(\w+?)\s+from\s+['"`].+?['"`]/g;
|
|
51
41
|
class CompletionsProviderImpl {
|
|
52
|
-
constructor(languageServiceManager) {
|
|
42
|
+
constructor(languageServiceManager, configManager) {
|
|
53
43
|
this.languageServiceManager = languageServiceManager;
|
|
44
|
+
this.configManager = configManager;
|
|
54
45
|
this.validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'];
|
|
55
46
|
}
|
|
56
47
|
isValidTriggerCharacter(character) {
|
|
57
48
|
return this.validTriggerCharacters.includes(character);
|
|
58
49
|
}
|
|
59
50
|
async getCompletions(document, position, completionContext, cancellationToken) {
|
|
60
|
-
const triggerCharacter = completionContext
|
|
61
|
-
const triggerKind = completionContext
|
|
51
|
+
const triggerCharacter = completionContext?.triggerCharacter;
|
|
52
|
+
const triggerKind = completionContext?.triggerKind;
|
|
62
53
|
const validTriggerCharacter = this.isValidTriggerCharacter(triggerCharacter) ? triggerCharacter : undefined;
|
|
63
54
|
const isCustomTriggerCharacter = triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter;
|
|
64
|
-
if ((isCustomTriggerCharacter && !validTriggerCharacter) ||
|
|
55
|
+
if ((isCustomTriggerCharacter && !validTriggerCharacter) || cancellationToken?.isCancellationRequested) {
|
|
65
56
|
return null;
|
|
66
57
|
}
|
|
67
58
|
if (this.canReuseLastCompletion(this.lastCompletion, triggerKind, triggerCharacter, document, position)) {
|
|
@@ -74,47 +65,76 @@ class CompletionsProviderImpl {
|
|
|
74
65
|
const html = document.html;
|
|
75
66
|
const offset = document.offsetAt(position);
|
|
76
67
|
const node = html.findNodeAt(offset);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
68
|
+
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
|
|
69
|
+
let filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
|
|
70
|
+
let completions;
|
|
81
71
|
const isCompletionInsideFrontmatter = (0, utils_1.isInsideFrontmatter)(document.getText(), offset);
|
|
82
72
|
const isCompletionInsideExpression = (0, utils_1.isInsideExpression)(document.getText(), node.start, offset);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
const tsPreferences = await this.configManager.getTSPreferences(document);
|
|
74
|
+
const formatOptions = await this.configManager.getTSFormatConfig(document);
|
|
75
|
+
let scriptTagIndex = undefined;
|
|
76
|
+
if (node.tag === 'script') {
|
|
77
|
+
const { filePath: scriptFilePath, offset: scriptOffset, index: scriptIndex, } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node, position);
|
|
78
|
+
filePath = scriptFilePath;
|
|
79
|
+
scriptTagIndex = scriptIndex;
|
|
80
|
+
completions = lang.getCompletionsAtPosition(scriptFilePath, scriptOffset, {
|
|
81
|
+
...tsPreferences,
|
|
82
|
+
// File extensions are required inside script tags, however TypeScript can't return completions with the `ts`
|
|
83
|
+
// extension, so what we'll do instead is force `minimal` (aka, no extension) and manually add the extensions
|
|
84
|
+
importModuleSpecifierEnding: 'minimal',
|
|
85
|
+
triggerCharacter: validTriggerCharacter,
|
|
86
|
+
}, formatOptions);
|
|
87
|
+
if (completions) {
|
|
88
|
+
// Manually adds file extensions to js and ts files
|
|
89
|
+
completions.entries = completions?.entries.map((comp) => {
|
|
90
|
+
if (comp.kind === typescript_1.ScriptElementKind.scriptElement &&
|
|
91
|
+
(comp.kindModifiers === '.js' || comp.kindModifiers === '.ts')) {
|
|
92
|
+
return {
|
|
93
|
+
...comp,
|
|
94
|
+
name: comp.name + comp.kindModifiers,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
return comp;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
94
102
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
103
|
+
else {
|
|
104
|
+
// PERF: Getting TS completions is fairly slow and I am currently not sure how to speed it up
|
|
105
|
+
// As such, we'll try to avoid getting them when unneeded, such as when we're doing HTML stuff
|
|
106
|
+
// When at the root of the document TypeScript offer all kinds of completions, because it doesn't know yet that
|
|
107
|
+
// it's JSX and not JS. As such, people who are using Emmet to write their template suffer from a very degraded experience
|
|
108
|
+
// from what they're used to in HTML files (which is instant completions). So let's disable ourselves when we're at the root
|
|
109
|
+
if (!isCompletionInsideFrontmatter && !node.parent && !isCompletionInsideExpression) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// If the user just typed `<` with nothing else, let's disable ourselves until we're more sure if the user wants TS completions
|
|
113
|
+
if (!isCompletionInsideFrontmatter && node.parent && node.tag === undefined && !isCompletionInsideExpression) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// If the current node is not a component (aka, it doesn't start with a caps), let's disable ourselves as the user
|
|
117
|
+
// is most likely looking for HTML completions
|
|
118
|
+
if (!isCompletionInsideFrontmatter && !(0, utils_1.isComponentTag)(node) && !isCompletionInsideExpression) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
completions = lang.getCompletionsAtPosition(filePath, offset, {
|
|
122
|
+
...tsPreferences,
|
|
123
|
+
triggerCharacter: validTriggerCharacter,
|
|
124
|
+
}, formatOptions);
|
|
99
125
|
}
|
|
100
|
-
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
|
|
101
|
-
const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
|
|
102
|
-
const completions = lang.getCompletionsAtPosition(filePath, offset, {
|
|
103
|
-
...completionOptions,
|
|
104
|
-
triggerCharacter: validTriggerCharacter,
|
|
105
|
-
});
|
|
106
126
|
if (completions === undefined || completions.entries.length === 0) {
|
|
107
127
|
return null;
|
|
108
128
|
}
|
|
109
129
|
const wordRange = completions.optionalReplacementSpan
|
|
110
130
|
? vscode_languageserver_1.Range.create(document.positionAt(completions.optionalReplacementSpan.start), document.positionAt(completions.optionalReplacementSpan.start + completions.optionalReplacementSpan.length))
|
|
111
131
|
: undefined;
|
|
112
|
-
const wordRangeStartPosition = wordRange
|
|
132
|
+
const wordRangeStartPosition = wordRange?.start;
|
|
113
133
|
const fragment = await tsDoc.createFragment();
|
|
114
134
|
const existingImports = this.getExistingImports(document);
|
|
115
135
|
const completionItems = completions.entries
|
|
116
136
|
.filter(this.isValidCompletion)
|
|
117
|
-
.map((entry) => this.toCompletionItem(fragment, entry, filePath, offset, isCompletionInsideFrontmatter, existingImports))
|
|
137
|
+
.map((entry) => this.toCompletionItem(fragment, entry, filePath, offset, isCompletionInsideFrontmatter, scriptTagIndex, existingImports))
|
|
118
138
|
.filter(utils_3.isNotNullOrUndefined)
|
|
119
139
|
.map((comp) => this.fixTextEditRange(wordRangeStartPosition, comp));
|
|
120
140
|
const completionList = vscode_languageserver_2.CompletionList.create(completionItems, true);
|
|
@@ -122,10 +142,10 @@ class CompletionsProviderImpl {
|
|
|
122
142
|
return completionList;
|
|
123
143
|
}
|
|
124
144
|
async resolveCompletion(document, item, cancellationToken) {
|
|
125
|
-
var _a;
|
|
126
145
|
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
|
|
146
|
+
const tsPreferences = await this.configManager.getTSPreferences(document);
|
|
127
147
|
const data = item.data;
|
|
128
|
-
if (!data || !data.filePath ||
|
|
148
|
+
if (!data || !data.filePath || cancellationToken?.isCancellationRequested) {
|
|
129
149
|
return item;
|
|
130
150
|
}
|
|
131
151
|
const fragment = await tsDoc.createFragment();
|
|
@@ -134,7 +154,7 @@ class CompletionsProviderImpl {
|
|
|
134
154
|
data.originalItem.name, // entryName
|
|
135
155
|
{}, // formatOptions
|
|
136
156
|
data.originalItem.source, // source
|
|
137
|
-
|
|
157
|
+
tsPreferences, // preferences
|
|
138
158
|
data.originalItem.data // data
|
|
139
159
|
);
|
|
140
160
|
if (detail) {
|
|
@@ -142,23 +162,33 @@ class CompletionsProviderImpl {
|
|
|
142
162
|
item.detail = itemDetail;
|
|
143
163
|
item.documentation = itemDocumentation;
|
|
144
164
|
}
|
|
145
|
-
const actions = detail
|
|
165
|
+
const actions = detail?.codeActions;
|
|
166
|
+
const isInsideScriptTag = data.scriptTagIndex !== undefined;
|
|
167
|
+
let scriptTagSnapshot;
|
|
168
|
+
if (isInsideScriptTag) {
|
|
169
|
+
const { snapshot } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, document.scriptTags[data.scriptTagIndex].container);
|
|
170
|
+
scriptTagSnapshot = snapshot;
|
|
171
|
+
}
|
|
146
172
|
if (actions) {
|
|
147
173
|
const edit = [];
|
|
148
174
|
for (const action of actions) {
|
|
149
175
|
for (const change of action.changes) {
|
|
150
|
-
|
|
176
|
+
if (isInsideScriptTag) {
|
|
177
|
+
change.textChanges.forEach((textChange) => {
|
|
178
|
+
textChange.span.start = fragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(textChange.span.start)));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
edit.push(...change.textChanges.map((textChange) => codeActionChangeToTextEdit(document, fragment, isInsideScriptTag, textChange)));
|
|
151
182
|
}
|
|
152
183
|
}
|
|
153
|
-
item.additionalTextEdits = (
|
|
184
|
+
item.additionalTextEdits = (item.additionalTextEdits ?? []).concat(edit);
|
|
154
185
|
}
|
|
155
186
|
return item;
|
|
156
187
|
}
|
|
157
|
-
toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, existingImports) {
|
|
158
|
-
var _a, _b, _c;
|
|
188
|
+
toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, scriptTagIndex, existingImports) {
|
|
159
189
|
let item = vscode_languageserver_protocol_1.CompletionItem.create(comp.name);
|
|
160
190
|
const isAstroComponent = this.isAstroComponentImport(comp.name);
|
|
161
|
-
const isImport =
|
|
191
|
+
const isImport = comp.insertText?.includes('import');
|
|
162
192
|
// Avoid showing completions for using components as functions
|
|
163
193
|
if (isAstroComponent && !isImport && insideFrontmatter) {
|
|
164
194
|
return null;
|
|
@@ -167,7 +197,7 @@ class CompletionsProviderImpl {
|
|
|
167
197
|
item.label = (0, utils_2.removeAstroComponentSuffix)(comp.name);
|
|
168
198
|
// Set component imports as file completion, that way we get cool icons
|
|
169
199
|
item.kind = vscode_languageserver_protocol_1.CompletionItemKind.File;
|
|
170
|
-
item.detail =
|
|
200
|
+
item.detail = comp.data?.moduleSpecifier;
|
|
171
201
|
}
|
|
172
202
|
else {
|
|
173
203
|
item.kind = (0, utils_2.scriptElementKindToCompletionItemKind)(comp.kind);
|
|
@@ -194,7 +224,7 @@ class CompletionsProviderImpl {
|
|
|
194
224
|
item.insertText = comp.insertText ? (0, utils_2.removeAstroComponentSuffix)(comp.insertText) : undefined;
|
|
195
225
|
item.insertTextFormat = comp.isSnippet ? vscode_languageserver_1.InsertTextFormat.Snippet : vscode_languageserver_1.InsertTextFormat.PlainText;
|
|
196
226
|
item.textEdit = comp.replacementSpan
|
|
197
|
-
? vscode_languageserver_1.TextEdit.replace((0, utils_2.convertRange)(fragment, comp.replacementSpan),
|
|
227
|
+
? vscode_languageserver_1.TextEdit.replace((0, utils_2.convertRange)(fragment, comp.replacementSpan), item.insertText ?? item.label)
|
|
198
228
|
: undefined;
|
|
199
229
|
}
|
|
200
230
|
return {
|
|
@@ -202,6 +232,7 @@ class CompletionsProviderImpl {
|
|
|
202
232
|
data: {
|
|
203
233
|
uri: fragment.getURL(),
|
|
204
234
|
filePath,
|
|
235
|
+
scriptTagIndex,
|
|
205
236
|
offset,
|
|
206
237
|
originalItem: comp,
|
|
207
238
|
},
|
|
@@ -214,20 +245,6 @@ class CompletionsProviderImpl {
|
|
|
214
245
|
}
|
|
215
246
|
return true;
|
|
216
247
|
}
|
|
217
|
-
codeActionChangeToTextEdit(document, fragment, change) {
|
|
218
|
-
change.newText = (0, utils_2.removeAstroComponentSuffix)(change.newText);
|
|
219
|
-
// If we don't have a frontmatter already, create one with the import
|
|
220
|
-
const frontmatterState = document.astroMeta.frontmatter.state;
|
|
221
|
-
if (frontmatterState === null) {
|
|
222
|
-
return vscode_languageserver_1.TextEdit.replace(vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0)), `---${typescript_1.default.sys.newLine}${change.newText}---${typescript_1.default.sys.newLine}${typescript_1.default.sys.newLine}`);
|
|
223
|
-
}
|
|
224
|
-
const { span } = change;
|
|
225
|
-
let range;
|
|
226
|
-
const virtualRange = (0, utils_2.convertRange)(fragment, span);
|
|
227
|
-
range = (0, documents_1.mapRangeToOriginal)(fragment, virtualRange);
|
|
228
|
-
range = (0, utils_2.ensureFrontmatterInsert)(range, document);
|
|
229
|
-
return vscode_languageserver_1.TextEdit.replace(range, change.newText);
|
|
230
|
-
}
|
|
231
248
|
getCompletionDocument(compDetail) {
|
|
232
249
|
const { sourceDisplay, documentation: tsDocumentation, displayParts } = compDetail;
|
|
233
250
|
let detail = (0, utils_2.removeAstroComponentSuffix)(typescript_1.default.displayPartsToString(displayParts));
|
|
@@ -286,7 +303,7 @@ class CompletionsProviderImpl {
|
|
|
286
303
|
(triggerCharacter === '.' && (0, utils_4.isPartOfImportStatement)(document.getText(), position))));
|
|
287
304
|
}
|
|
288
305
|
getExistingImports(document) {
|
|
289
|
-
const rawImports = (0, utils_3.getRegExpMatches)(scriptImportRegex, document.getText()).map((match) =>
|
|
306
|
+
const rawImports = (0, utils_3.getRegExpMatches)(scriptImportRegex, document.getText()).map((match) => (match[1] ?? match[2]).split(','));
|
|
290
307
|
const tidiedImports = (0, lodash_1.flatten)(rawImports).map((match) => match.trim());
|
|
291
308
|
return new Set(tidiedImports);
|
|
292
309
|
}
|
|
@@ -295,3 +312,34 @@ class CompletionsProviderImpl {
|
|
|
295
312
|
}
|
|
296
313
|
}
|
|
297
314
|
exports.CompletionsProviderImpl = CompletionsProviderImpl;
|
|
315
|
+
function codeActionChangeToTextEdit(document, fragment, isInsideScriptTag, change) {
|
|
316
|
+
change.newText = (0, utils_2.removeAstroComponentSuffix)(change.newText);
|
|
317
|
+
const { span } = change;
|
|
318
|
+
let range;
|
|
319
|
+
const virtualRange = (0, utils_2.convertRange)(fragment, span);
|
|
320
|
+
range = (0, documents_1.mapRangeToOriginal)(fragment, virtualRange);
|
|
321
|
+
if (!isInsideScriptTag) {
|
|
322
|
+
// If we don't have a frontmatter already, create one with the import
|
|
323
|
+
const frontmatterState = document.astroMeta.frontmatter.state;
|
|
324
|
+
if (frontmatterState === null) {
|
|
325
|
+
return vscode_languageserver_1.TextEdit.replace(vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0)), `---${typescript_1.default.sys.newLine}${change.newText}---${typescript_1.default.sys.newLine}${typescript_1.default.sys.newLine}`);
|
|
326
|
+
}
|
|
327
|
+
if (!(0, utils_1.isInsideFrontmatter)(document.getText(), document.offsetAt(range.start))) {
|
|
328
|
+
range = (0, utils_2.ensureFrontmatterInsert)(range, document);
|
|
329
|
+
}
|
|
330
|
+
// First import in a file will wrongly have a newline before it due to how the frontmatter is replaced by a comment
|
|
331
|
+
if (range.start.line === 1 && (change.newText.startsWith('\n') || change.newText.startsWith('\r\n'))) {
|
|
332
|
+
change.newText = change.newText.trimStart();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const existingLine = (0, utils_1.getLineAtPosition)(document.positionAt(span.start), document.getText());
|
|
337
|
+
const isNewImport = !existingLine.trim().startsWith('import');
|
|
338
|
+
// Avoid putting new imports on the same line as the script tag opening
|
|
339
|
+
if (!(change.newText.startsWith('\n') || change.newText.startsWith('\r\n')) && isNewImport) {
|
|
340
|
+
change.newText = typescript_1.default.sys.newLine + change.newText;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return vscode_languageserver_1.TextEdit.replace(range, change.newText);
|
|
344
|
+
}
|
|
345
|
+
exports.codeActionChangeToTextEdit = codeActionChangeToTextEdit;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Position, LocationLink } from 'vscode-languageserver-types';
|
|
2
|
+
import { AstroDocument } from '../../../core/documents';
|
|
3
|
+
import { DefinitionsProvider } from '../../interfaces';
|
|
4
|
+
import { LanguageServiceManager } from '../LanguageServiceManager';
|
|
5
|
+
export declare class DefinitionsProviderImpl implements DefinitionsProvider {
|
|
6
|
+
private languageServiceManager;
|
|
7
|
+
constructor(languageServiceManager: LanguageServiceManager);
|
|
8
|
+
getDefinitions(document: AstroDocument, position: Position): Promise<LocationLink[]>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DefinitionsProviderImpl = void 0;
|
|
4
|
+
const vscode_languageserver_types_1 = require("vscode-languageserver-types");
|
|
5
|
+
const utils_1 = require("../../../utils");
|
|
6
|
+
const utils_2 = require("../utils");
|
|
7
|
+
const utils_3 = require("./utils");
|
|
8
|
+
class DefinitionsProviderImpl {
|
|
9
|
+
constructor(languageServiceManager) {
|
|
10
|
+
this.languageServiceManager = languageServiceManager;
|
|
11
|
+
}
|
|
12
|
+
async getDefinitions(document, position) {
|
|
13
|
+
const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
|
|
14
|
+
const mainFragment = await tsDoc.createFragment();
|
|
15
|
+
const tsFilePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
|
|
16
|
+
const fragmentPosition = mainFragment.getGeneratedPosition(position);
|
|
17
|
+
const fragmentOffset = mainFragment.offsetAt(fragmentPosition);
|
|
18
|
+
let defs;
|
|
19
|
+
const html = document.html;
|
|
20
|
+
const offset = document.offsetAt(position);
|
|
21
|
+
const node = html.findNodeAt(offset);
|
|
22
|
+
if (node.tag === 'script') {
|
|
23
|
+
const { snapshot: scriptTagSnapshot, filePath: scriptFilePath, offset: scriptOffset, } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node, position);
|
|
24
|
+
defs = lang.getDefinitionAndBoundSpan(scriptFilePath, scriptOffset);
|
|
25
|
+
if (defs) {
|
|
26
|
+
defs.definitions = defs.definitions?.map((def) => {
|
|
27
|
+
const isInSameFile = def.fileName === scriptFilePath;
|
|
28
|
+
def.fileName = isInSameFile ? tsFilePath : def.fileName;
|
|
29
|
+
if (isInSameFile) {
|
|
30
|
+
def.textSpan.start = mainFragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(def.textSpan.start)));
|
|
31
|
+
}
|
|
32
|
+
return def;
|
|
33
|
+
});
|
|
34
|
+
defs.textSpan.start = mainFragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(defs.textSpan.start)));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
defs = lang.getDefinitionAndBoundSpan(tsFilePath, fragmentOffset);
|
|
39
|
+
}
|
|
40
|
+
if (!defs || !defs.definitions) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const docs = new utils_3.SnapshotFragmentMap(this.languageServiceManager);
|
|
44
|
+
docs.set(tsFilePath, { fragment: mainFragment, snapshot: tsDoc });
|
|
45
|
+
const result = await Promise.all(defs.definitions.map(async (def) => {
|
|
46
|
+
const { fragment, snapshot } = await docs.retrieve(def.fileName);
|
|
47
|
+
const fileName = (0, utils_2.ensureRealFilePath)(def.fileName);
|
|
48
|
+
// For Astro, Svelte and Vue, the position is wrongly mapped to the end of the file due to the TSX output
|
|
49
|
+
// So we'll instead redirect to the beginning of the file
|
|
50
|
+
const isFramework = (0, utils_2.isFrameworkFilePath)(def.fileName) || (0, utils_2.isAstroFilePath)(def.fileName);
|
|
51
|
+
const textSpan = isFramework && tsDoc.filePath !== def.fileName ? { start: 0, length: 0 } : def.textSpan;
|
|
52
|
+
return vscode_languageserver_types_1.LocationLink.create((0, utils_1.pathToUrl)(fileName), (0, utils_2.convertToLocationRange)(fragment, textSpan), (0, utils_2.convertToLocationRange)(fragment, textSpan), (0, utils_2.convertToLocationRange)(mainFragment, defs.textSpan));
|
|
53
|
+
}));
|
|
54
|
+
return result.filter(utils_1.isNotNullOrUndefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.DefinitionsProviderImpl = DefinitionsProviderImpl;
|