@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/check.js +1 -2
  3. package/dist/core/config/ConfigManager.d.ts +20 -16
  4. package/dist/core/config/ConfigManager.js +112 -46
  5. package/dist/core/config/interfaces.d.ts +0 -52
  6. package/dist/core/documents/AstroDocument.d.ts +1 -0
  7. package/dist/core/documents/AstroDocument.js +1 -0
  8. package/dist/core/documents/DocumentMapper.d.ts +2 -0
  9. package/dist/core/documents/DocumentMapper.js +9 -9
  10. package/dist/core/documents/parseAstro.js +1 -1
  11. package/dist/core/documents/utils.d.ts +1 -0
  12. package/dist/core/documents/utils.js +19 -5
  13. package/dist/plugins/PluginHost.d.ts +2 -1
  14. package/dist/plugins/PluginHost.js +8 -6
  15. package/dist/plugins/astro/AstroPlugin.d.ts +1 -6
  16. package/dist/plugins/astro/AstroPlugin.js +1 -83
  17. package/dist/plugins/astro/features/CompletionsProvider.d.ts +4 -5
  18. package/dist/plugins/astro/features/CompletionsProvider.js +49 -59
  19. package/dist/plugins/css/CSSPlugin.d.ts +5 -5
  20. package/dist/plugins/css/CSSPlugin.js +41 -20
  21. package/dist/plugins/html/HTMLPlugin.d.ts +4 -4
  22. package/dist/plugins/html/HTMLPlugin.js +20 -16
  23. package/dist/plugins/html/features/astro-attributes.js +44 -27
  24. package/dist/plugins/typescript/LanguageServiceManager.js +1 -1
  25. package/dist/plugins/typescript/TypeScriptPlugin.d.ts +5 -4
  26. package/dist/plugins/typescript/TypeScriptPlugin.js +30 -108
  27. package/dist/plugins/typescript/astro-sys.js +3 -5
  28. package/dist/plugins/typescript/astro2tsx.js +1 -2
  29. package/dist/plugins/typescript/features/CodeActionsProvider.d.ts +16 -0
  30. package/dist/plugins/typescript/features/CodeActionsProvider.js +206 -0
  31. package/dist/plugins/typescript/features/CompletionsProvider.d.ts +5 -2
  32. package/dist/plugins/typescript/features/CompletionsProvider.js +116 -68
  33. package/dist/plugins/typescript/features/DefinitionsProvider.d.ts +9 -0
  34. package/dist/plugins/typescript/features/DefinitionsProvider.js +57 -0
  35. package/dist/plugins/typescript/features/DiagnosticsProvider.js +60 -18
  36. package/dist/plugins/typescript/features/DocumentSymbolsProvider.js +3 -4
  37. package/dist/plugins/typescript/features/FoldingRangesProvider.js +13 -6
  38. package/dist/plugins/typescript/features/HoverProvider.js +14 -1
  39. package/dist/plugins/typescript/features/SemanticTokenProvider.js +1 -1
  40. package/dist/plugins/typescript/features/SignatureHelpProvider.js +11 -3
  41. package/dist/plugins/typescript/features/utils.d.ts +2 -0
  42. package/dist/plugins/typescript/features/utils.js +19 -3
  43. package/dist/plugins/typescript/language-service.js +23 -6
  44. package/dist/plugins/typescript/module-loader.js +1 -1
  45. package/dist/plugins/typescript/previewer.js +1 -1
  46. package/dist/plugins/typescript/snapshots/DocumentSnapshot.d.ts +22 -2
  47. package/dist/plugins/typescript/snapshots/DocumentSnapshot.js +48 -1
  48. package/dist/plugins/typescript/snapshots/SnapshotManager.js +2 -1
  49. package/dist/plugins/typescript/snapshots/utils.js +3 -6
  50. package/dist/plugins/typescript/utils.d.ts +12 -1
  51. package/dist/plugins/typescript/utils.js +29 -1
  52. package/dist/server.js +43 -14
  53. package/dist/utils.d.ts +4 -0
  54. package/dist/utils.js +16 -3
  55. 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
- constructor(languageServiceManager: LanguageServiceManager);
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 === null || completionContext === void 0 ? void 0 : completionContext.triggerCharacter;
61
- const triggerKind = completionContext === null || completionContext === void 0 ? void 0 : completionContext.triggerKind;
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) || (cancellationToken === null || cancellationToken === void 0 ? void 0 : cancellationToken.isCancellationRequested)) {
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
- // TODO: Add support for script tags
78
- if (node.tag === 'script') {
79
- return null;
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
- // PERF: Getting TS completions is fairly slow and I am currently not sure how to speed it up
84
- // As such, we'll try to avoid getting them when unneeded, such as when we're doing HTML stuff
85
- // When at the root of the document TypeScript offer all kinds of completions, because it doesn't know yet that
86
- // it's JSX and not JS. As such, people who are using Emmet to write their template suffer from a very degraded experience
87
- // from what they're used to in HTML files (which is instant completions). So let's disable ourselves when we're at the root
88
- if (!isCompletionInsideFrontmatter && !node.parent && !isCompletionInsideExpression) {
89
- return null;
90
- }
91
- // If the user just typed `<` with nothing else, let's disable ourselves until we're more sure if the user wants TS completions
92
- if (!isCompletionInsideFrontmatter && node.parent && node.tag === undefined && !isCompletionInsideExpression) {
93
- return null;
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
- // If the current node is not a component (aka, it doesn't start with a caps), let's disable ourselves as the user
96
- // is most likely looking for HTML completions
97
- if (!isCompletionInsideFrontmatter && !(0, utils_1.isComponentTag)(node) && !isCompletionInsideExpression) {
98
- return null;
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 === null || wordRange === void 0 ? void 0 : wordRange.start;
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 || (cancellationToken === null || cancellationToken === void 0 ? void 0 : cancellationToken.isCancellationRequested)) {
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
- completionOptions, // preferences
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 === null || detail === void 0 ? void 0 : detail.codeActions;
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
- edit.push(...change.textChanges.map((textChange) => this.codeActionChangeToTextEdit(document, fragment, textChange)));
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 = ((_a = item.additionalTextEdits) !== null && _a !== void 0 ? _a : []).concat(edit);
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 = (_a = comp.insertText) === null || _a === void 0 ? void 0 : _a.includes('import');
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 = (_b = comp.data) === null || _b === void 0 ? void 0 : _b.moduleSpecifier;
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), (_c = item.insertText) !== null && _c !== void 0 ? _c : item.label)
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) => { var _a; return ((_a = match[1]) !== null && _a !== void 0 ? _a : match[2]).split(','); });
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;