@astrojs/language-server 0.16.0 → 0.18.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 (47) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/core/config/ConfigManager.d.ts +25 -16
  3. package/dist/core/config/ConfigManager.js +160 -46
  4. package/dist/core/config/interfaces.d.ts +4 -51
  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 +3 -1
  12. package/dist/plugins/PluginHost.js +8 -0
  13. package/dist/plugins/astro/AstroPlugin.d.ts +1 -6
  14. package/dist/plugins/astro/AstroPlugin.js +10 -85
  15. package/dist/plugins/astro/features/CompletionsProvider.d.ts +4 -5
  16. package/dist/plugins/astro/features/CompletionsProvider.js +53 -58
  17. package/dist/plugins/css/CSSPlugin.d.ts +5 -5
  18. package/dist/plugins/css/CSSPlugin.js +36 -31
  19. package/dist/plugins/html/HTMLPlugin.d.ts +6 -5
  20. package/dist/plugins/html/HTMLPlugin.js +38 -16
  21. package/dist/plugins/html/features/astro-attributes.js +1 -0
  22. package/dist/plugins/interfaces.d.ts +5 -2
  23. package/dist/plugins/typescript/TypeScriptPlugin.d.ts +7 -4
  24. package/dist/plugins/typescript/TypeScriptPlugin.js +34 -110
  25. package/dist/plugins/typescript/features/CodeActionsProvider.d.ts +3 -1
  26. package/dist/plugins/typescript/features/CodeActionsProvider.js +82 -17
  27. package/dist/plugins/typescript/features/CompletionsProvider.d.ts +5 -3
  28. package/dist/plugins/typescript/features/CompletionsProvider.js +112 -56
  29. package/dist/plugins/typescript/features/DefinitionsProvider.d.ts +9 -0
  30. package/dist/plugins/typescript/features/DefinitionsProvider.js +57 -0
  31. package/dist/plugins/typescript/features/DiagnosticsProvider.js +58 -15
  32. package/dist/plugins/typescript/features/FoldingRangesProvider.js +13 -6
  33. package/dist/plugins/typescript/features/FormattingProvider.d.ts +11 -0
  34. package/dist/plugins/typescript/features/FormattingProvider.js +132 -0
  35. package/dist/plugins/typescript/features/HoverProvider.js +14 -1
  36. package/dist/plugins/typescript/features/InlayHintsProvider.d.ts +12 -0
  37. package/dist/plugins/typescript/features/InlayHintsProvider.js +36 -0
  38. package/dist/plugins/typescript/features/SignatureHelpProvider.js +9 -1
  39. package/dist/plugins/typescript/language-service.js +18 -0
  40. package/dist/plugins/typescript/snapshots/DocumentSnapshot.d.ts +22 -2
  41. package/dist/plugins/typescript/snapshots/DocumentSnapshot.js +48 -1
  42. package/dist/plugins/typescript/snapshots/SnapshotManager.js +1 -0
  43. package/dist/plugins/typescript/snapshots/utils.js +3 -2
  44. package/dist/plugins/typescript/utils.d.ts +11 -1
  45. package/dist/plugins/typescript/utils.js +17 -1
  46. package/dist/server.js +27 -15
  47. package/package.json +7 -6
@@ -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.codeActionChangeToTextEdit = exports.CompletionsProviderImpl = exports.completionOptions = 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,22 +35,13 @@ 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
- exports.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) {
@@ -74,35 +65,64 @@ 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
- ...exports.completionOptions,
104
- triggerCharacter: validTriggerCharacter,
105
- });
106
126
  if (completions === undefined || completions.entries.length === 0) {
107
127
  return null;
108
128
  }
@@ -114,7 +134,7 @@ class CompletionsProviderImpl {
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);
@@ -123,6 +143,7 @@ class CompletionsProviderImpl {
123
143
  }
124
144
  async resolveCompletion(document, item, cancellationToken) {
125
145
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
146
+ const tsPreferences = await this.configManager.getTSPreferences(document);
126
147
  const data = item.data;
127
148
  if (!data || !data.filePath || cancellationToken?.isCancellationRequested) {
128
149
  return item;
@@ -133,27 +154,42 @@ class CompletionsProviderImpl {
133
154
  data.originalItem.name, // entryName
134
155
  {}, // formatOptions
135
156
  data.originalItem.source, // source
136
- exports.completionOptions, // preferences
157
+ tsPreferences, // preferences
137
158
  data.originalItem.data // data
138
159
  );
139
160
  if (detail) {
140
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
+ // }
141
166
  item.detail = itemDetail;
142
167
  item.documentation = itemDocumentation;
143
168
  }
144
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
+ }
145
176
  if (actions) {
146
177
  const edit = [];
147
178
  for (const action of actions) {
148
179
  for (const change of action.changes) {
149
- 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)));
150
186
  }
151
187
  }
152
188
  item.additionalTextEdits = (item.additionalTextEdits ?? []).concat(edit);
153
189
  }
154
190
  return item;
155
191
  }
156
- toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, existingImports) {
192
+ toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, scriptTagIndex, existingImports) {
157
193
  let item = vscode_languageserver_protocol_1.CompletionItem.create(comp.name);
158
194
  const isAstroComponent = this.isAstroComponentImport(comp.name);
159
195
  const isImport = comp.insertText?.includes('import');
@@ -177,14 +213,23 @@ class CompletionsProviderImpl {
177
213
  }
178
214
  if (comp.kindModifiers) {
179
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
+ }
180
225
  if (kindModifiers.has(typescript_1.ScriptElementKindModifier.deprecatedModifier)) {
181
226
  item.tags = [vscode_languageserver_1.CompletionItemTag.Deprecated];
182
227
  }
183
228
  }
184
- // Label details are currently unsupported, however, they'll be supported in the next version of LSP
185
- if (comp.sourceDisplay) {
186
- item.labelDetails = { description: typescript_1.default.displayPartsToString(comp.sourceDisplay) };
187
- }
229
+ // TODO: Add support for labelDetails
230
+ // if (comp.sourceDisplay) {
231
+ // item.labelDetails = { description: ts.displayPartsToString(comp.sourceDisplay) };
232
+ // }
188
233
  item.commitCharacters = (0, utils_2.getCommitCharactersForScriptElement)(comp.kind);
189
234
  item.sortText = comp.sortText;
190
235
  item.preselect = comp.isRecommended;
@@ -200,6 +245,7 @@ class CompletionsProviderImpl {
200
245
  data: {
201
246
  uri: fragment.getURL(),
202
247
  filePath,
248
+ scriptTagIndex,
203
249
  offset,
204
250
  originalItem: comp,
205
251
  },
@@ -279,23 +325,33 @@ class CompletionsProviderImpl {
279
325
  }
280
326
  }
281
327
  exports.CompletionsProviderImpl = CompletionsProviderImpl;
282
- function codeActionChangeToTextEdit(document, fragment, change) {
328
+ function codeActionChangeToTextEdit(document, fragment, isInsideScriptTag, change) {
283
329
  change.newText = (0, utils_2.removeAstroComponentSuffix)(change.newText);
284
- // If we don't have a frontmatter already, create one with the import
285
- const frontmatterState = document.astroMeta.frontmatter.state;
286
- if (frontmatterState === null) {
287
- 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}`);
288
- }
289
330
  const { span } = change;
290
331
  let range;
291
332
  const virtualRange = (0, utils_2.convertRange)(fragment, span);
292
333
  range = (0, documents_1.mapRangeToOriginal)(fragment, virtualRange);
293
- if (!(0, utils_1.isInsideFrontmatter)(document.getText(), document.offsetAt(range.start))) {
294
- 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
+ }
295
347
  }
296
- // First import in a file will wrongly have a newline before it due to how the frontmatter is replaced by a comment
297
- if (range.start.line === 1 && (change.newText.startsWith('\n') || change.newText.startsWith('\r\n'))) {
298
- 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
+ }
299
355
  }
300
356
  return vscode_languageserver_1.TextEdit.replace(range, change.newText);
301
357
  }
@@ -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;
@@ -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
+ }
@@ -0,0 +1,132 @@
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.FormattingProviderImpl = void 0;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ const vscode_languageserver_types_1 = require("vscode-languageserver-types");
9
+ const utils_1 = require("../utils");
10
+ class FormattingProviderImpl {
11
+ constructor(languageServiceManager, configManager) {
12
+ this.languageServiceManager = languageServiceManager;
13
+ this.configManager = configManager;
14
+ }
15
+ async formatDocument(document, options) {
16
+ const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
17
+ const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
18
+ const formatConfig = await this.configManager.getTSFormatConfig(document, options);
19
+ let frontmatterEdits = [];
20
+ let scriptTagsEdits = [];
21
+ if (document.astroMeta.frontmatter.state === 'closed') {
22
+ const start = document.positionAt(document.astroMeta.frontmatter.startOffset + 3);
23
+ start.line += 1;
24
+ start.character = 0;
25
+ const startOffset = document.offsetAt(start);
26
+ const endOffset = document.astroMeta.frontmatter.endOffset;
27
+ const astroFormatConfig = await this.configManager.getAstroFormatConfig(document);
28
+ const settings = {
29
+ ...formatConfig,
30
+ baseIndentSize: astroFormatConfig.indentFrontmatter ? formatConfig.tabSize ?? 0 : undefined,
31
+ };
32
+ frontmatterEdits = lang.getFormattingEditsForRange(filePath, startOffset, endOffset, settings);
33
+ if (astroFormatConfig.newLineAfterFrontmatter) {
34
+ const templateStart = document.positionAt(endOffset + 3);
35
+ templateStart.line += 1;
36
+ templateStart.character = 0;
37
+ frontmatterEdits.push({
38
+ span: { start: document.offsetAt(templateStart), length: 0 },
39
+ newText: '\n',
40
+ });
41
+ }
42
+ }
43
+ document.scriptTags.forEach((scriptTag) => {
44
+ const { filePath: scriptFilePath, snapshot: scriptTagSnapshot } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
45
+ const startLine = document.offsetAt(vscode_languageserver_types_1.Position.create(scriptTag.startPos.line, 0));
46
+ const initialIndentLevel = computeInitialIndent(document, startLine, options);
47
+ const baseIndent = (formatConfig.tabSize ?? 0) * (initialIndentLevel + 1);
48
+ const formatSettings = {
49
+ baseIndentSize: baseIndent,
50
+ indentStyle: typescript_1.default.IndentStyle.Smart,
51
+ ...formatConfig,
52
+ };
53
+ let edits = lang.getFormattingEditsForDocument(scriptFilePath, formatSettings);
54
+ if (edits) {
55
+ edits = edits
56
+ .map((edit) => {
57
+ edit.span.start = document.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(edit.span.start)));
58
+ return edit;
59
+ })
60
+ .filter((edit) => {
61
+ return (scriptTagSnapshot.isInGenerated(document.positionAt(edit.span.start)) &&
62
+ scriptTag.end !== edit.span.start &&
63
+ // Don't format the last line of the file as it's in most case the indentation
64
+ scriptTag.endPos.line !== document.positionAt(edit.span.start).line);
65
+ });
66
+ const endLine = document.getLineUntilOffset(document.offsetAt(scriptTag.endPos));
67
+ if (isWhitespaceOnly(endLine)) {
68
+ const endLineStartOffset = document.offsetAt(vscode_languageserver_types_1.Position.create(scriptTag.endPos.line, 0));
69
+ const lastLineIndentRange = vscode_languageserver_types_1.Range.create(vscode_languageserver_types_1.Position.create(scriptTag.endPos.line, 0), scriptTag.endPos);
70
+ const newText = generateIndent(initialIndentLevel, options);
71
+ if (endLine !== newText) {
72
+ edits.push({
73
+ span: {
74
+ start: endLineStartOffset,
75
+ length: lastLineIndentRange.end.character,
76
+ },
77
+ newText,
78
+ });
79
+ }
80
+ }
81
+ }
82
+ scriptTagsEdits.push(...edits);
83
+ });
84
+ return [...frontmatterEdits, ...scriptTagsEdits].map((edit) => ({
85
+ range: (0, utils_1.convertRange)(document, edit.span),
86
+ newText: edit.newText,
87
+ }));
88
+ }
89
+ }
90
+ exports.FormattingProviderImpl = FormattingProviderImpl;
91
+ function computeInitialIndent(document, lineStart, options) {
92
+ let content = document.getText();
93
+ let i = lineStart;
94
+ let nChars = 0;
95
+ let tabSize = options.tabSize || 4;
96
+ while (i < content.length) {
97
+ let ch = content.charAt(i);
98
+ if (ch === ' ') {
99
+ nChars++;
100
+ }
101
+ else if (ch === '\t') {
102
+ nChars += tabSize;
103
+ }
104
+ else {
105
+ break;
106
+ }
107
+ i++;
108
+ }
109
+ return Math.floor(nChars / tabSize);
110
+ }
111
+ function generateIndent(level, options) {
112
+ if (options.insertSpaces) {
113
+ return repeat(' ', level * options.tabSize);
114
+ }
115
+ else {
116
+ return repeat('\t', level);
117
+ }
118
+ }
119
+ function repeat(value, count) {
120
+ let s = '';
121
+ while (count > 0) {
122
+ if ((count & 1) === 1) {
123
+ s += value;
124
+ }
125
+ value += value;
126
+ count = count >>> 1;
127
+ }
128
+ return s;
129
+ }
130
+ function isWhitespaceOnly(str) {
131
+ return /^\s*$/.test(str);
132
+ }
@@ -18,7 +18,20 @@ class HoverProviderImpl {
18
18
  const fragment = await tsDoc.createFragment();
19
19
  const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
20
20
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
21
- let info = lang.getQuickInfoAtPosition(filePath, offset);
21
+ const html = document.html;
22
+ const documentOffset = document.offsetAt(position);
23
+ const node = html.findNodeAt(documentOffset);
24
+ let info;
25
+ if (node.tag === 'script') {
26
+ const { snapshot: scriptTagSnapshot, filePath: scriptFilePath, offset: scriptOffset, } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, node, position);
27
+ info = lang.getQuickInfoAtPosition(scriptFilePath, scriptOffset);
28
+ if (info) {
29
+ info.textSpan.start = fragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(info.textSpan.start)));
30
+ }
31
+ }
32
+ else {
33
+ info = lang.getQuickInfoAtPosition(filePath, offset);
34
+ }
22
35
  if (!info) {
23
36
  return null;
24
37
  }