@astrojs/language-server 0.16.1 → 0.18.1

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 (38) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/core/config/ConfigManager.d.ts +4 -3
  3. package/dist/core/config/ConfigManager.js +15 -1
  4. package/dist/core/config/interfaces.d.ts +5 -0
  5. package/dist/core/documents/AstroDocument.d.ts +1 -0
  6. package/dist/core/documents/AstroDocument.js +1 -0
  7. package/dist/core/documents/DocumentMapper.d.ts +2 -0
  8. package/dist/core/documents/DocumentMapper.js +7 -5
  9. package/dist/core/documents/utils.d.ts +1 -0
  10. package/dist/core/documents/utils.js +16 -1
  11. package/dist/plugins/PluginHost.d.ts +2 -1
  12. package/dist/plugins/PluginHost.js +4 -0
  13. package/dist/plugins/astro/AstroPlugin.js +10 -3
  14. package/dist/plugins/astro/features/CompletionsProvider.d.ts +4 -5
  15. package/dist/plugins/astro/features/CompletionsProvider.js +53 -58
  16. package/dist/plugins/html/HTMLPlugin.d.ts +2 -1
  17. package/dist/plugins/html/HTMLPlugin.js +18 -0
  18. package/dist/plugins/typescript/TypeScriptPlugin.d.ts +3 -1
  19. package/dist/plugins/typescript/TypeScriptPlugin.js +5 -0
  20. package/dist/plugins/typescript/features/CodeActionsProvider.js +76 -17
  21. package/dist/plugins/typescript/features/CompletionsProvider.d.ts +2 -1
  22. package/dist/plugins/typescript/features/CompletionsProvider.js +107 -45
  23. package/dist/plugins/typescript/features/DefinitionsProvider.js +22 -1
  24. package/dist/plugins/typescript/features/DiagnosticsProvider.js +58 -15
  25. package/dist/plugins/typescript/features/FoldingRangesProvider.js +13 -6
  26. package/dist/plugins/typescript/features/FormattingProvider.d.ts +11 -0
  27. package/dist/plugins/typescript/features/FormattingProvider.js +132 -0
  28. package/dist/plugins/typescript/features/HoverProvider.js +14 -1
  29. package/dist/plugins/typescript/features/SignatureHelpProvider.js +9 -1
  30. package/dist/plugins/typescript/language-service.js +18 -0
  31. package/dist/plugins/typescript/snapshots/DocumentSnapshot.d.ts +22 -2
  32. package/dist/plugins/typescript/snapshots/DocumentSnapshot.js +48 -1
  33. package/dist/plugins/typescript/snapshots/SnapshotManager.js +1 -0
  34. package/dist/plugins/typescript/snapshots/utils.js +3 -2
  35. package/dist/plugins/typescript/utils.d.ts +11 -1
  36. package/dist/plugins/typescript/utils.js +17 -1
  37. package/dist/server.js +2 -0
  38. package/package.json +3 -2
@@ -23,8 +23,6 @@ class CodeActionsProviderImpl {
23
23
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
24
24
  const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
25
25
  const fragment = await tsDoc.createFragment();
26
- const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
27
- const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
28
26
  const tsPreferences = await this.configManager.getTSPreferences(document);
29
27
  const formatOptions = await this.configManager.getTSFormatConfig(document);
30
28
  let result = [];
@@ -50,24 +48,57 @@ class CodeActionsProviderImpl {
50
48
  .map((diag) => Number(diag.code))
51
49
  // We currently cannot support quick fix for unreachable code properly due to the way our TSX output is structured
52
50
  .filter((code) => code !== 7027);
53
- let codeFixes = errorCodes.includes(2304)
54
- ? this.getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences)
55
- : undefined;
56
- codeFixes =
57
- codeFixes ?? lang.getCodeFixesAtPosition(filePath, start, end, errorCodes, formatOptions, tsPreferences);
58
- const codeActions = codeFixes.map((fix) => codeFixToCodeAction(fix, context.diagnostics, context.only ? vscode_languageserver_types_1.CodeActionKind.QuickFix : vscode_languageserver_types_1.CodeActionKind.Empty));
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));
59
76
  result.push(...codeActions);
60
77
  }
61
78
  return result;
62
- function codeFixToCodeAction(codeFix, diagnostics, kind) {
79
+ function codeFixToCodeAction(codeFix, diagnostics, kind, isInsideScript) {
63
80
  const documentChanges = codeFix.changes.map((change) => {
64
81
  return vscode_languageserver_types_1.TextDocumentEdit.create(vscode_languageserver_types_1.OptionalVersionedTextDocumentIdentifier.create(document.getURL(), null), change.textChanges.map((edit) => {
65
82
  let originalRange = (0, documents_1.mapRangeToOriginal)(fragment, (0, utils_2.convertRange)(fragment, edit.span));
66
- if (codeFix.fixName === 'import') {
67
- return (0, CompletionsProvider_1.codeActionChangeToTextEdit)(document, fragment, edit);
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
+ }
68
92
  }
69
- if (codeFix.fixName === 'fixMissingFunctionDeclaration') {
70
- originalRange = (0, utils_2.checkEndOfFileCodeInsert)(originalRange, document);
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
+ }
71
102
  }
72
103
  return vscode_languageserver_types_1.TextEdit.replace(originalRange, edit.newText);
73
104
  }));
@@ -78,6 +109,15 @@ class CodeActionsProviderImpl {
78
109
  codeAction.diagnostics = diagnostics;
79
110
  return codeAction;
80
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
+ }
81
121
  }
82
122
  getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences) {
83
123
  const sourceFile = lang.getProgram()?.getSourceFile(filePath);
@@ -107,16 +147,35 @@ class CodeActionsProviderImpl {
107
147
  return (0, lodash_1.flatten)(completion.entries.filter((c) => c.name === name || c.name === suffixedName).map(toFix));
108
148
  }
109
149
  async organizeSortImports(document, skipDestructiveCodeActions = false, cancellationToken) {
110
- if (document.astroMeta.frontmatter.state !== 'closed') {
111
- return [];
112
- }
113
150
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
114
151
  const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
115
152
  const fragment = await tsDoc.createFragment();
116
153
  if (cancellationToken?.isCancellationRequested) {
117
154
  return [];
118
155
  }
119
- const changes = lang.organizeImports({ fileName: filePath, type: 'file', skipDestructiveCodeActions }, {}, {});
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
+ });
120
179
  const documentChanges = changes.map((change) => {
121
180
  return vscode_languageserver_types_1.TextDocumentEdit.create(vscode_languageserver_types_1.OptionalVersionedTextDocumentIdentifier.create(document.url, null), change.textChanges.map((edit) => {
122
181
  const range = (0, documents_1.mapRangeToOriginal)(fragment, (0, utils_2.convertRange)(fragment, edit.span));
@@ -8,6 +8,7 @@ import { ConfigManager } from '../../../core/config';
8
8
  export interface CompletionItemData extends TextDocumentIdentifier {
9
9
  filePath: string;
10
10
  offset: number;
11
+ scriptTagIndex: number | undefined;
11
12
  originalItem: ts.CompletionEntry;
12
13
  }
13
14
  export declare class CompletionsProviderImpl implements CompletionsProvider<CompletionItemData> {
@@ -32,4 +33,4 @@ export declare class CompletionsProviderImpl implements CompletionsProvider<Comp
32
33
  private getExistingImports;
33
34
  private isAstroComponentImport;
34
35
  }
35
- export declare function codeActionChangeToTextEdit(document: AstroDocument, fragment: AstroSnapshotFragment, change: ts.TextChange): TextEdit;
36
+ export declare function codeActionChangeToTextEdit(document: AstroDocument, fragment: AstroSnapshotFragment, isInsideScriptTag: boolean, change: ts.TextChange): TextEdit;
@@ -65,37 +65,64 @@ class CompletionsProviderImpl {
65
65
  const html = document.html;
66
66
  const offset = document.offsetAt(position);
67
67
  const node = html.findNodeAt(offset);
68
- // TODO: Add support for script tags
69
- if (node.tag === 'script') {
70
- return null;
71
- }
68
+ const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
69
+ let filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
70
+ let completions;
72
71
  const isCompletionInsideFrontmatter = (0, utils_1.isInsideFrontmatter)(document.getText(), offset);
73
72
  const isCompletionInsideExpression = (0, utils_1.isInsideExpression)(document.getText(), node.start, offset);
74
- // PERF: Getting TS completions is fairly slow and I am currently not sure how to speed it up
75
- // As such, we'll try to avoid getting them when unneeded, such as when we're doing HTML stuff
76
- // When at the root of the document TypeScript offer all kinds of completions, because it doesn't know yet that
77
- // it's JSX and not JS. As such, people who are using Emmet to write their template suffer from a very degraded experience
78
- // from what they're used to in HTML files (which is instant completions). So let's disable ourselves when we're at the root
79
- if (!isCompletionInsideFrontmatter && !node.parent && !isCompletionInsideExpression) {
80
- return null;
81
- }
82
- // If the user just typed `<` with nothing else, let's disable ourselves until we're more sure if the user wants TS completions
83
- if (!isCompletionInsideFrontmatter && node.parent && node.tag === undefined && !isCompletionInsideExpression) {
84
- return null;
85
- }
86
- // If the current node is not a component (aka, it doesn't start with a caps), let's disable ourselves as the user
87
- // is most likely looking for HTML completions
88
- if (!isCompletionInsideFrontmatter && !(0, utils_1.isComponentTag)(node) && !isCompletionInsideExpression) {
89
- return null;
90
- }
91
73
  const tsPreferences = await this.configManager.getTSPreferences(document);
92
74
  const formatOptions = await this.configManager.getTSFormatConfig(document);
93
- const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
94
- const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
95
- const completions = lang.getCompletionsAtPosition(filePath, offset, {
96
- ...tsPreferences,
97
- triggerCharacter: validTriggerCharacter,
98
- }, formatOptions);
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
+ }
102
+ }
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);
125
+ }
99
126
  if (completions === undefined || completions.entries.length === 0) {
100
127
  return null;
101
128
  }
@@ -107,7 +134,7 @@ class CompletionsProviderImpl {
107
134
  const existingImports = this.getExistingImports(document);
108
135
  const completionItems = completions.entries
109
136
  .filter(this.isValidCompletion)
110
- .map((entry) => this.toCompletionItem(fragment, entry, filePath, offset, isCompletionInsideFrontmatter, existingImports))
137
+ .map((entry) => this.toCompletionItem(fragment, entry, filePath, offset, isCompletionInsideFrontmatter, scriptTagIndex, existingImports))
111
138
  .filter(utils_3.isNotNullOrUndefined)
112
139
  .map((comp) => this.fixTextEditRange(wordRangeStartPosition, comp));
113
140
  const completionList = vscode_languageserver_2.CompletionList.create(completionItems, true);
@@ -132,22 +159,37 @@ class CompletionsProviderImpl {
132
159
  );
133
160
  if (detail) {
134
161
  const { detail: itemDetail, documentation: itemDocumentation } = this.getCompletionDocument(detail);
162
+ // TODO: Add support for labelDetails
163
+ // if (data.originalItem.source) {
164
+ // item.labelDetails = { description: data.originalItem.source };
165
+ // }
135
166
  item.detail = itemDetail;
136
167
  item.documentation = itemDocumentation;
137
168
  }
138
169
  const actions = detail?.codeActions;
170
+ const isInsideScriptTag = data.scriptTagIndex !== undefined;
171
+ let scriptTagSnapshot;
172
+ if (isInsideScriptTag) {
173
+ const { snapshot } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, document.scriptTags[data.scriptTagIndex].container);
174
+ scriptTagSnapshot = snapshot;
175
+ }
139
176
  if (actions) {
140
177
  const edit = [];
141
178
  for (const action of actions) {
142
179
  for (const change of action.changes) {
143
- edit.push(...change.textChanges.map((textChange) => codeActionChangeToTextEdit(document, fragment, textChange)));
180
+ if (isInsideScriptTag) {
181
+ change.textChanges.forEach((textChange) => {
182
+ textChange.span.start = fragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(textChange.span.start)));
183
+ });
184
+ }
185
+ edit.push(...change.textChanges.map((textChange) => codeActionChangeToTextEdit(document, fragment, isInsideScriptTag, textChange)));
144
186
  }
145
187
  }
146
188
  item.additionalTextEdits = (item.additionalTextEdits ?? []).concat(edit);
147
189
  }
148
190
  return item;
149
191
  }
150
- toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, existingImports) {
192
+ toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, scriptTagIndex, existingImports) {
151
193
  let item = vscode_languageserver_protocol_1.CompletionItem.create(comp.name);
152
194
  const isAstroComponent = this.isAstroComponentImport(comp.name);
153
195
  const isImport = comp.insertText?.includes('import');
@@ -171,14 +213,23 @@ class CompletionsProviderImpl {
171
213
  }
172
214
  if (comp.kindModifiers) {
173
215
  const kindModifiers = new Set(comp.kindModifiers.split(/,|\s+/g));
216
+ if (kindModifiers.has(typescript_1.ScriptElementKindModifier.optionalModifier)) {
217
+ if (!item.insertText) {
218
+ item.insertText = item.label;
219
+ }
220
+ if (!item.filterText) {
221
+ item.filterText = item.label;
222
+ }
223
+ item.label += '?';
224
+ }
174
225
  if (kindModifiers.has(typescript_1.ScriptElementKindModifier.deprecatedModifier)) {
175
226
  item.tags = [vscode_languageserver_1.CompletionItemTag.Deprecated];
176
227
  }
177
228
  }
178
- // Label details are currently unsupported, however, they'll be supported in the next version of LSP
179
- if (comp.sourceDisplay) {
180
- item.labelDetails = { description: typescript_1.default.displayPartsToString(comp.sourceDisplay) };
181
- }
229
+ // TODO: Add support for labelDetails
230
+ // if (comp.sourceDisplay) {
231
+ // item.labelDetails = { description: ts.displayPartsToString(comp.sourceDisplay) };
232
+ // }
182
233
  item.commitCharacters = (0, utils_2.getCommitCharactersForScriptElement)(comp.kind);
183
234
  item.sortText = comp.sortText;
184
235
  item.preselect = comp.isRecommended;
@@ -194,6 +245,7 @@ class CompletionsProviderImpl {
194
245
  data: {
195
246
  uri: fragment.getURL(),
196
247
  filePath,
248
+ scriptTagIndex,
197
249
  offset,
198
250
  originalItem: comp,
199
251
  },
@@ -273,23 +325,33 @@ class CompletionsProviderImpl {
273
325
  }
274
326
  }
275
327
  exports.CompletionsProviderImpl = CompletionsProviderImpl;
276
- function codeActionChangeToTextEdit(document, fragment, change) {
328
+ function codeActionChangeToTextEdit(document, fragment, isInsideScriptTag, change) {
277
329
  change.newText = (0, utils_2.removeAstroComponentSuffix)(change.newText);
278
- // If we don't have a frontmatter already, create one with the import
279
- const frontmatterState = document.astroMeta.frontmatter.state;
280
- if (frontmatterState === null) {
281
- 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}`);
282
- }
283
330
  const { span } = change;
284
331
  let range;
285
332
  const virtualRange = (0, utils_2.convertRange)(fragment, span);
286
333
  range = (0, documents_1.mapRangeToOriginal)(fragment, virtualRange);
287
- if (!(0, utils_1.isInsideFrontmatter)(document.getText(), document.offsetAt(range.start))) {
288
- range = (0, utils_2.ensureFrontmatterInsert)(range, document);
334
+ if (!isInsideScriptTag) {
335
+ // If we don't have a frontmatter already, create one with the import
336
+ const frontmatterState = document.astroMeta.frontmatter.state;
337
+ if (frontmatterState === null) {
338
+ 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}`);
339
+ }
340
+ if (!(0, utils_1.isInsideFrontmatter)(document.getText(), document.offsetAt(range.start))) {
341
+ range = (0, utils_2.ensureFrontmatterInsert)(range, document);
342
+ }
343
+ // First import in a file will wrongly have a newline before it due to how the frontmatter is replaced by a comment
344
+ if (range.start.line === 1 && (change.newText.startsWith('\n') || change.newText.startsWith('\r\n'))) {
345
+ change.newText = change.newText.trimStart();
346
+ }
289
347
  }
290
- // First import in a file will wrongly have a newline before it due to how the frontmatter is replaced by a comment
291
- if (range.start.line === 1 && (change.newText.startsWith('\n') || change.newText.startsWith('\r\n'))) {
292
- change.newText = change.newText.trimStart();
348
+ else {
349
+ const existingLine = (0, utils_1.getLineAtPosition)(document.positionAt(span.start), document.getText());
350
+ const isNewImport = !existingLine.trim().startsWith('import');
351
+ // Avoid putting new imports on the same line as the script tag opening
352
+ if (!(change.newText.startsWith('\n') || change.newText.startsWith('\r\n')) && isNewImport) {
353
+ change.newText = typescript_1.default.sys.newLine + change.newText;
354
+ }
293
355
  }
294
356
  return vscode_languageserver_1.TextEdit.replace(range, change.newText);
295
357
  }
@@ -15,7 +15,28 @@ class DefinitionsProviderImpl {
15
15
  const tsFilePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
16
16
  const fragmentPosition = mainFragment.getGeneratedPosition(position);
17
17
  const fragmentOffset = mainFragment.offsetAt(fragmentPosition);
18
- const defs = lang.getDefinitionAndBoundSpan(tsFilePath, fragmentOffset);
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
+ }
19
40
  if (!defs || !defs.definitions) {
20
41
  return [];
21
42
  }
@@ -20,24 +20,52 @@ class DiagnosticsProviderImpl {
20
20
  }
21
21
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
22
22
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
23
+ const fragment = await tsDoc.createFragment();
24
+ let scriptDiagnostics = [];
25
+ document.scriptTags.forEach((scriptTag) => {
26
+ const { filePath: scriptFilePath, snapshot: scriptTagSnapshot } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
27
+ const scriptDiagnostic = [
28
+ ...lang.getSyntacticDiagnostics(scriptFilePath),
29
+ ...lang.getSuggestionDiagnostics(scriptFilePath),
30
+ ...lang.getSemanticDiagnostics(scriptFilePath),
31
+ ]
32
+ // We need to duplicate the diagnostic creation here because we can't map TS's diagnostics range to the original
33
+ // file due to some internal cache inside TS that would cause it to being mapped twice in some cases
34
+ .map((diagnostic) => ({
35
+ range: (0, utils_1.convertRange)(scriptTagSnapshot, diagnostic),
36
+ severity: (0, utils_1.mapSeverity)(diagnostic.category),
37
+ source: 'ts',
38
+ message: typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
39
+ code: diagnostic.code,
40
+ tags: getDiagnosticTag(diagnostic),
41
+ }))
42
+ .map(mapRange(scriptTagSnapshot, document));
43
+ scriptDiagnostics.push(...scriptDiagnostic);
44
+ });
23
45
  const { script: scriptBoundaries } = this.getTagBoundaries(lang, filePath);
24
46
  const syntaxDiagnostics = lang.getSyntacticDiagnostics(filePath);
25
47
  const suggestionDiagnostics = lang.getSuggestionDiagnostics(filePath);
26
- const semanticDiagnostics = lang.getSemanticDiagnostics(filePath).filter((d) => {
27
- return isNoWithinBoundary(scriptBoundaries, d);
48
+ const semanticDiagnostics = lang.getSemanticDiagnostics(filePath);
49
+ const diagnostics = [
50
+ ...syntaxDiagnostics,
51
+ ...suggestionDiagnostics,
52
+ ...semanticDiagnostics,
53
+ ].filter((diag) => {
54
+ return isNoWithinBoundary(scriptBoundaries, diag);
28
55
  });
29
- const diagnostics = [...syntaxDiagnostics, ...suggestionDiagnostics, ...semanticDiagnostics];
30
- const fragment = await tsDoc.createFragment();
31
- return diagnostics
32
- .map((diagnostic) => ({
33
- range: (0, utils_1.convertRange)(tsDoc, diagnostic),
34
- severity: (0, utils_1.mapSeverity)(diagnostic.category),
35
- source: 'ts',
36
- message: typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
37
- code: diagnostic.code,
38
- tags: getDiagnosticTag(diagnostic),
39
- }))
40
- .map(mapRange(fragment, document))
56
+ return [
57
+ ...diagnostics
58
+ .map((diagnostic) => ({
59
+ range: (0, utils_1.convertRange)(tsDoc, diagnostic),
60
+ severity: (0, utils_1.mapSeverity)(diagnostic.category),
61
+ source: 'ts',
62
+ message: typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
63
+ code: diagnostic.code,
64
+ tags: getDiagnosticTag(diagnostic),
65
+ }))
66
+ .map(mapRange(fragment, document)),
67
+ ...scriptDiagnostics,
68
+ ]
41
69
  .filter((diag) => {
42
70
  return (hasNoNegativeLines(diag) &&
43
71
  isNoJSXImplicitRuntimeWarning(diag) &&
@@ -46,6 +74,7 @@ class DiagnosticsProviderImpl {
46
74
  isNoSpreadExpected(diag) &&
47
75
  isNoCantResolveJSONModule(diag) &&
48
76
  isNoCantReturnOutsideFunction(diag) &&
77
+ isNoIsolatedModuleError(diag) &&
49
78
  isNoJsxCannotHaveMultipleAttrsError(diag));
50
79
  })
51
80
  .map(enhanceIfNecessary);
@@ -146,12 +175,26 @@ function isNoCantReturnOutsideFunction(diagnostic) {
146
175
  function isNoCantResolveJSONModule(diagnostic) {
147
176
  return diagnostic.code !== 2732;
148
177
  }
178
+ /**
179
+ * When the content of the file is invalid and can't be parsed properly for TSX generation, TS will show an error about
180
+ * how the current module can't be compiled under --isolatedModule, this is confusing to users so let's ignore this
181
+ */
182
+ function isNoIsolatedModuleError(diagnostic) {
183
+ return diagnostic.code !== 1208;
184
+ }
149
185
  /**
150
186
  * Some diagnostics have JSX-specific nomenclature or unclear description. Enhance them for more clarity.
151
187
  */
152
188
  function enhanceIfNecessary(diagnostic) {
189
+ // JSX element has no closing tag. JSX -> HTML
190
+ if (diagnostic.code === 17008) {
191
+ return {
192
+ ...diagnostic,
193
+ message: diagnostic.message.replace('JSX', 'HTML'),
194
+ };
195
+ }
196
+ // For the rare case where an user might try to put a client directive on something that is not a component
153
197
  if (diagnostic.code === 2322) {
154
- // For the rare case where an user might try to put a client directive on something that is not a component
155
198
  if (diagnostic.message.includes("Property 'client:") && diagnostic.message.includes("to type 'HTMLProps")) {
156
199
  return {
157
200
  ...diagnostic,
@@ -15,15 +15,22 @@ class FoldingRangesProviderImpl {
15
15
  const html = document.html;
16
16
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
17
17
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
18
- const outliningSpans = lang.getOutliningSpans(filePath);
19
- const foldingRanges = [];
20
- for (const span of outliningSpans) {
18
+ const outliningSpans = lang.getOutliningSpans(filePath).filter((span) => {
21
19
  const node = html.findNodeAt(span.textSpan.start);
22
20
  // Due to how our TSX output transform those tags into function calls or template literals
23
21
  // TypeScript thinks of those as outlining spans, which is fine but we don't want folding ranges for those
24
- if (node.tag === 'script' || node.tag === 'Markdown' || node.tag === 'style') {
25
- continue;
26
- }
22
+ return node.tag !== 'script' && node.tag !== 'style' && node.tag !== 'Markdown';
23
+ });
24
+ const scriptOutliningSpans = [];
25
+ document.scriptTags.forEach((scriptTag) => {
26
+ const { snapshot: scriptTagSnapshot, filePath: scriptFilePath } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
27
+ scriptOutliningSpans.push(...lang.getOutliningSpans(scriptFilePath).map((span) => {
28
+ span.textSpan.start = document.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(span.textSpan.start)));
29
+ return span;
30
+ }));
31
+ });
32
+ const foldingRanges = [];
33
+ for (const span of [...outliningSpans, ...scriptOutliningSpans]) {
27
34
  const start = document.positionAt(span.textSpan.start);
28
35
  const end = adjustFoldingEnd(start, document.positionAt(span.textSpan.start + span.textSpan.length), document);
29
36
  // When using this method for generating folding ranges, TypeScript tend to return some
@@ -0,0 +1,11 @@
1
+ import { FormattingOptions, TextEdit } from 'vscode-languageserver-types';
2
+ import { ConfigManager } from '../../../core/config';
3
+ import { AstroDocument } from '../../../core/documents';
4
+ import { FormattingProvider } from '../../interfaces';
5
+ import { LanguageServiceManager } from '../LanguageServiceManager';
6
+ export declare class FormattingProviderImpl implements FormattingProvider {
7
+ private languageServiceManager;
8
+ private configManager;
9
+ constructor(languageServiceManager: LanguageServiceManager, configManager: ConfigManager);
10
+ formatDocument(document: AstroDocument, options: FormattingOptions): Promise<TextEdit[]>;
11
+ }