@astrojs/language-server 0.13.3 → 0.15.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 (39) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/core/documents/utils.d.ts +6 -0
  3. package/dist/core/documents/utils.js +17 -1
  4. package/dist/plugins/PluginHost.d.ts +1 -1
  5. package/dist/plugins/PluginHost.js +29 -4
  6. package/dist/plugins/astro/AstroPlugin.d.ts +1 -1
  7. package/dist/plugins/astro/AstroPlugin.js +1 -1
  8. package/dist/plugins/astro/features/CompletionsProvider.d.ts +2 -2
  9. package/dist/plugins/astro/features/CompletionsProvider.js +24 -9
  10. package/dist/plugins/css/CSSPlugin.js +9 -1
  11. package/dist/plugins/css/features/astro-selectors.js +1 -2
  12. package/dist/plugins/html/features/astro-attributes.js +56 -1
  13. package/dist/plugins/html/utils.d.ts +1 -1
  14. package/dist/plugins/html/utils.js +1 -1
  15. package/dist/plugins/interfaces.d.ts +2 -2
  16. package/dist/plugins/typescript/TypeScriptPlugin.d.ts +6 -4
  17. package/dist/plugins/typescript/TypeScriptPlugin.js +12 -4
  18. package/dist/plugins/typescript/astro2tsx.d.ts +1 -1
  19. package/dist/plugins/typescript/astro2tsx.js +11 -6
  20. package/dist/plugins/typescript/features/CompletionsProvider.d.ts +23 -6
  21. package/dist/plugins/typescript/features/CompletionsProvider.js +256 -51
  22. package/dist/plugins/typescript/features/DiagnosticsProvider.js +44 -58
  23. package/dist/plugins/typescript/features/DocumentSymbolsProvider.js +8 -5
  24. package/dist/plugins/typescript/features/FoldingRangesProvider.d.ts +9 -0
  25. package/dist/plugins/typescript/features/FoldingRangesProvider.js +64 -0
  26. package/dist/plugins/typescript/features/SemanticTokenProvider.js +4 -1
  27. package/dist/plugins/typescript/features/utils.d.ts +2 -0
  28. package/dist/plugins/typescript/features/utils.js +7 -1
  29. package/dist/plugins/typescript/language-service.d.ts +1 -1
  30. package/dist/plugins/typescript/language-service.js +44 -23
  31. package/dist/plugins/typescript/snapshots/SnapshotManager.js +2 -1
  32. package/dist/plugins/typescript/snapshots/utils.js +24 -3
  33. package/dist/plugins/typescript/utils.d.ts +3 -0
  34. package/dist/plugins/typescript/utils.js +17 -1
  35. package/dist/server.js +16 -0
  36. package/dist/utils.d.ts +16 -0
  37. package/dist/utils.js +57 -1
  38. package/package.json +4 -4
  39. package/types/astro-jsx.d.ts +112 -113
@@ -1,92 +1,297 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
4
24
  };
5
25
  Object.defineProperty(exports, "__esModule", { value: true });
6
26
  exports.CompletionsProviderImpl = void 0;
7
- const utils_1 = require("../../../core/documents/utils");
8
- const typescript_1 = __importDefault(require("typescript"));
9
27
  const vscode_languageserver_1 = require("vscode-languageserver");
28
+ const vscode_languageserver_protocol_1 = require("vscode-languageserver-protocol");
29
+ const utils_1 = require("../../../core/documents/utils");
30
+ const documents_1 = require("../../../core/documents");
31
+ const typescript_1 = __importStar(require("typescript"));
32
+ const vscode_languageserver_2 = require("vscode-languageserver");
10
33
  const utils_2 = require("../utils");
11
- const completionOptions = Object.freeze({
34
+ const utils_3 = require("../../../utils");
35
+ const lodash_1 = require("lodash");
36
+ const previewer_1 = require("../previewer");
37
+ const utils_4 = require("./utils");
38
+ const completionOptions = {
12
39
  importModuleSpecifierPreference: 'relative',
13
40
  importModuleSpecifierEnding: 'auto',
14
41
  quotePreference: 'single',
15
- });
42
+ includeCompletionsForModuleExports: true,
43
+ includeCompletionsForImportStatements: true,
44
+ includeCompletionsWithInsertText: true,
45
+ allowIncompleteCompletions: true,
46
+ includeCompletionsWithSnippetText: true,
47
+ };
48
+ // `import {...} from '..'` or `import ... from '..'`
49
+ // Note: Does not take into account if import is within a comment.
50
+ const scriptImportRegex = /\bimport\s+{([^}]*?)}\s+?from\s+['"`].+?['"`]|\bimport\s+(\w+?)\s+from\s+['"`].+?['"`]/g;
16
51
  class CompletionsProviderImpl {
17
52
  constructor(languageServiceManager) {
18
53
  this.languageServiceManager = languageServiceManager;
54
+ this.validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'];
19
55
  }
20
- async getCompletions(document, position, _completionContext) {
21
- var _a;
22
- // TODO: handle inside expression and script tags
23
- if (!(0, utils_1.isInsideFrontmatter)(document.getText(), document.offsetAt(position))) {
56
+ isValidTriggerCharacter(character) {
57
+ return this.validTriggerCharacters.includes(character);
58
+ }
59
+ 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;
62
+ const validTriggerCharacter = this.isValidTriggerCharacter(triggerCharacter) ? triggerCharacter : undefined;
63
+ const isCustomTriggerCharacter = triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter;
64
+ if ((isCustomTriggerCharacter && !validTriggerCharacter) || (cancellationToken === null || cancellationToken === void 0 ? void 0 : cancellationToken.isCancellationRequested)) {
65
+ return null;
66
+ }
67
+ if (this.canReuseLastCompletion(this.lastCompletion, triggerKind, triggerCharacter, document, position)) {
68
+ this.lastCompletion.position = position;
69
+ return this.lastCompletion.completionList;
70
+ }
71
+ else {
72
+ this.lastCompletion = undefined;
73
+ }
74
+ const html = document.html;
75
+ const offset = document.offsetAt(position);
76
+ const node = html.findNodeAt(offset);
77
+ // TODO: Add support for script tags
78
+ if (node.tag === 'script') {
79
+ return null;
80
+ }
81
+ const isCompletionInsideFrontmatter = (0, utils_1.isInsideFrontmatter)(document.getText(), offset);
82
+ 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;
94
+ }
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) {
24
98
  return null;
25
99
  }
26
100
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
27
101
  const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
102
+ const completions = lang.getCompletionsAtPosition(filePath, offset, {
103
+ ...completionOptions,
104
+ triggerCharacter: validTriggerCharacter,
105
+ });
106
+ if (completions === undefined || completions.entries.length === 0) {
107
+ return null;
108
+ }
109
+ const wordRange = completions.optionalReplacementSpan
110
+ ? vscode_languageserver_1.Range.create(document.positionAt(completions.optionalReplacementSpan.start), document.positionAt(completions.optionalReplacementSpan.start + completions.optionalReplacementSpan.length))
111
+ : undefined;
112
+ const wordRangeStartPosition = wordRange === null || wordRange === void 0 ? void 0 : wordRange.start;
28
113
  const fragment = await tsDoc.createFragment();
29
- const offset = document.offsetAt(position);
30
- const entries = ((_a = lang.getCompletionsAtPosition(filePath, offset, completionOptions)) === null || _a === void 0 ? void 0 : _a.entries) || [];
31
- const completionItems = entries
32
- .map((entry) => this.toCompletionItem(fragment, entry, document.uri, position, new Set()))
33
- .filter((i) => i);
34
- return vscode_languageserver_1.CompletionList.create(completionItems, true);
114
+ const existingImports = this.getExistingImports(document);
115
+ const completionItems = completions.entries
116
+ .filter(this.isValidCompletion)
117
+ .map((entry) => this.toCompletionItem(fragment, entry, filePath, offset, isCompletionInsideFrontmatter, existingImports))
118
+ .filter(utils_3.isNotNullOrUndefined)
119
+ .map((comp) => this.fixTextEditRange(wordRangeStartPosition, comp));
120
+ const completionList = vscode_languageserver_2.CompletionList.create(completionItems, true);
121
+ this.lastCompletion = { key: document.getFilePath() || '', position, completionList };
122
+ return completionList;
35
123
  }
36
- async resolveCompletion(document, completionItem) {
37
- const { data: comp } = completionItem;
124
+ async resolveCompletion(document, item, cancellationToken) {
125
+ var _a;
38
126
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
39
- let filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
40
- if (!comp || !filePath) {
41
- return completionItem;
127
+ const data = item.data;
128
+ if (!data || !data.filePath || (cancellationToken === null || cancellationToken === void 0 ? void 0 : cancellationToken.isCancellationRequested)) {
129
+ return item;
42
130
  }
43
131
  const fragment = await tsDoc.createFragment();
44
- const detail = lang.getCompletionEntryDetails(filePath, // fileName
45
- fragment.offsetAt(comp.position), // position
46
- comp.name, // entryName
132
+ const detail = lang.getCompletionEntryDetails(data.filePath, // fileName
133
+ data.offset, // position
134
+ data.originalItem.name, // entryName
47
135
  {}, // formatOptions
48
- comp.source, // source
49
- {}, // preferences
50
- comp.data // data
136
+ data.originalItem.source, // source
137
+ completionOptions, // preferences
138
+ data.originalItem.data // data
51
139
  );
52
140
  if (detail) {
53
141
  const { detail: itemDetail, documentation: itemDocumentation } = this.getCompletionDocument(detail);
54
- completionItem.detail = itemDetail;
55
- completionItem.documentation = itemDocumentation;
142
+ item.detail = itemDetail;
143
+ item.documentation = itemDocumentation;
56
144
  }
57
- return completionItem;
145
+ const actions = detail === null || detail === void 0 ? void 0 : detail.codeActions;
146
+ if (actions) {
147
+ const edit = [];
148
+ for (const action of actions) {
149
+ for (const change of action.changes) {
150
+ edit.push(...change.textChanges.map((textChange) => this.codeActionChangeToTextEdit(document, fragment, textChange)));
151
+ }
152
+ }
153
+ item.additionalTextEdits = ((_a = item.additionalTextEdits) !== null && _a !== void 0 ? _a : []).concat(edit);
154
+ }
155
+ return item;
58
156
  }
59
- toCompletionItem(fragment, comp, uri, position, existingImports) {
157
+ toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, existingImports) {
158
+ var _a, _b, _c;
159
+ let item = vscode_languageserver_protocol_1.CompletionItem.create(comp.name);
160
+ const isAstroComponent = this.isAstroComponentImport(comp.name);
161
+ const isImport = (_a = comp.insertText) === null || _a === void 0 ? void 0 : _a.includes('import');
162
+ // Avoid showing completions for using components as functions
163
+ if (isAstroComponent && !isImport && insideFrontmatter) {
164
+ return null;
165
+ }
166
+ if (isAstroComponent) {
167
+ item.label = (0, utils_2.removeAstroComponentSuffix)(comp.name);
168
+ // Set component imports as file completion, that way we get cool icons
169
+ item.kind = vscode_languageserver_protocol_1.CompletionItemKind.File;
170
+ item.detail = (_b = comp.data) === null || _b === void 0 ? void 0 : _b.moduleSpecifier;
171
+ }
172
+ else {
173
+ item.kind = (0, utils_2.scriptElementKindToCompletionItemKind)(comp.kind);
174
+ }
175
+ // TS may suggest another component even if there already exists an import with the same.
176
+ // This happens because internally, components get suffixed with __AstroComponent_
177
+ if (isAstroComponent && existingImports.has(item.label)) {
178
+ return null;
179
+ }
180
+ if (comp.kindModifiers) {
181
+ const kindModifiers = new Set(comp.kindModifiers.split(/,|\s+/g));
182
+ if (kindModifiers.has(typescript_1.ScriptElementKindModifier.deprecatedModifier)) {
183
+ item.tags = [vscode_languageserver_1.CompletionItemTag.Deprecated];
184
+ }
185
+ }
186
+ // Label details are currently unsupported, however, they'll be supported in the next version of LSP
187
+ if (comp.sourceDisplay) {
188
+ item.labelDetails = { description: typescript_1.default.displayPartsToString(comp.sourceDisplay) };
189
+ }
190
+ item.commitCharacters = (0, utils_2.getCommitCharactersForScriptElement)(comp.kind);
191
+ item.sortText = comp.sortText;
192
+ item.preselect = comp.isRecommended;
193
+ if (comp.replacementSpan) {
194
+ item.insertText = comp.insertText ? (0, utils_2.removeAstroComponentSuffix)(comp.insertText) : undefined;
195
+ item.insertTextFormat = comp.isSnippet ? vscode_languageserver_1.InsertTextFormat.Snippet : vscode_languageserver_1.InsertTextFormat.PlainText;
196
+ 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)
198
+ : undefined;
199
+ }
60
200
  return {
61
- label: comp.name,
62
- insertText: comp.insertText,
63
- kind: (0, utils_2.scriptElementKindToCompletionItemKind)(comp.kind),
64
- commitCharacters: (0, utils_2.getCommitCharactersForScriptElement)(comp.kind),
65
- // Make sure svelte component takes precedence
66
- sortText: comp.sortText,
67
- preselect: comp.isRecommended,
68
- // pass essential data for resolving completion
201
+ ...item,
69
202
  data: {
70
- ...comp,
71
- uri,
72
- position,
203
+ uri: fragment.getURL(),
204
+ filePath,
205
+ offset,
206
+ originalItem: comp,
73
207
  },
74
208
  };
75
209
  }
210
+ isValidCompletion(completion) {
211
+ // Remove completion for default exported function
212
+ if (completion.name === 'default' && completion.kindModifiers == typescript_1.ScriptElementKindModifier.exportedModifier) {
213
+ return false;
214
+ }
215
+ return true;
216
+ }
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
+ }
76
231
  getCompletionDocument(compDetail) {
77
- const { source, documentation: tsDocumentation, displayParts, tags } = compDetail;
78
- let detail = typescript_1.default.displayPartsToString(displayParts);
79
- if (source) {
80
- const importPath = typescript_1.default.displayPartsToString(source);
81
- detail = `Auto import from ${importPath}\n${detail}`;
82
- }
83
- const documentation = tsDocumentation
84
- ? { value: tsDocumentation.join('\n'), kind: vscode_languageserver_1.MarkupKind.Markdown }
85
- : undefined;
232
+ const { sourceDisplay, documentation: tsDocumentation, displayParts } = compDetail;
233
+ let detail = (0, utils_2.removeAstroComponentSuffix)(typescript_1.default.displayPartsToString(displayParts));
234
+ if (sourceDisplay) {
235
+ const importPath = typescript_1.default.displayPartsToString(sourceDisplay);
236
+ detail = importPath;
237
+ }
238
+ const documentation = {
239
+ kind: 'markdown',
240
+ value: (0, previewer_1.getMarkdownDocumentation)(tsDocumentation, compDetail.tags),
241
+ };
86
242
  return {
87
243
  documentation,
88
244
  detail,
89
245
  };
90
246
  }
247
+ /**
248
+ * If the textEdit is out of the word range of the triggered position
249
+ * vscode would refuse to show the completions
250
+ * split those edits into additionalTextEdit to fix it
251
+ */
252
+ fixTextEditRange(wordRangePosition, completionItem) {
253
+ const { textEdit } = completionItem;
254
+ if (!textEdit || !vscode_languageserver_1.TextEdit.is(textEdit) || !wordRangePosition) {
255
+ return completionItem;
256
+ }
257
+ const { newText, range: { start }, } = textEdit;
258
+ const wordRangeStartCharacter = wordRangePosition.character;
259
+ if (wordRangePosition.line !== wordRangePosition.line || start.character > wordRangePosition.character) {
260
+ return completionItem;
261
+ }
262
+ textEdit.newText = newText.substring(wordRangeStartCharacter - start.character);
263
+ textEdit.range.start = {
264
+ line: start.line,
265
+ character: wordRangeStartCharacter,
266
+ };
267
+ completionItem.additionalTextEdits = [
268
+ vscode_languageserver_1.TextEdit.replace({
269
+ start,
270
+ end: {
271
+ line: start.line,
272
+ character: wordRangeStartCharacter,
273
+ },
274
+ }, newText.substring(0, wordRangeStartCharacter - start.character)),
275
+ ];
276
+ return completionItem;
277
+ }
278
+ canReuseLastCompletion(lastCompletion, triggerKind, triggerCharacter, document, position) {
279
+ return (!!lastCompletion &&
280
+ lastCompletion.key === document.getFilePath() &&
281
+ lastCompletion.position.line === position.line &&
282
+ Math.abs(lastCompletion.position.character - position.character) < 2 &&
283
+ (triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerForIncompleteCompletions ||
284
+ // Special case: `.` is a trigger character, but inside import path completions
285
+ // it shouldn't trigger another completion because we can reuse the old one
286
+ (triggerCharacter === '.' && (0, utils_4.isPartOfImportStatement)(document.getText(), position))));
287
+ }
288
+ 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(','); });
290
+ const tidiedImports = (0, lodash_1.flatten)(rawImports).map((match) => match.trim());
291
+ return new Set(tidiedImports);
292
+ }
293
+ isAstroComponentImport(className) {
294
+ return className.endsWith('__AstroComponent_');
295
+ }
91
296
  }
92
297
  exports.CompletionsProviderImpl = CompletionsProviderImpl;
@@ -13,7 +13,7 @@ class DiagnosticsProviderImpl {
13
13
  this.languageServiceManager = languageServiceManager;
14
14
  }
15
15
  async getDiagnostics(document, _cancellationToken) {
16
- var _a, _b, _c;
16
+ var _a, _b;
17
17
  // Don't return diagnostics for files inside node_modules. These are considered read-only
18
18
  // and they would pollute the output for astro check
19
19
  if (((_a = document.getFilePath()) === null || _a === void 0 ? void 0 : _a.includes('/node_modules/')) || ((_b = document.getFilePath()) === null || _b === void 0 ? void 0 : _b.includes('\\node_modules\\'))) {
@@ -21,16 +21,14 @@ class DiagnosticsProviderImpl {
21
21
  }
22
22
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
23
23
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
24
- const { script: scriptBoundaries, markdown: markdownBoundaries } = this.getTagBoundaries(lang, filePath);
24
+ const { script: scriptBoundaries } = this.getTagBoundaries(lang, filePath);
25
25
  const syntaxDiagnostics = lang.getSyntacticDiagnostics(filePath);
26
26
  const suggestionDiagnostics = lang.getSuggestionDiagnostics(filePath);
27
27
  const semanticDiagnostics = lang.getSemanticDiagnostics(filePath).filter((d) => {
28
- return isNoWithinScript(scriptBoundaries, d);
28
+ return isNoWithinBoundary(scriptBoundaries, d);
29
29
  });
30
30
  const diagnostics = [...syntaxDiagnostics, ...suggestionDiagnostics, ...semanticDiagnostics];
31
31
  const fragment = await tsDoc.createFragment();
32
- const sourceFile = (_c = lang.getProgram()) === null || _c === void 0 ? void 0 : _c.getSourceFile(filePath);
33
- const isNoFalsePositiveInst = isNoFalsePositive();
34
32
  return diagnostics
35
33
  .map((diagnostic) => ({
36
34
  range: (0, utils_1.convertRange)(tsDoc, diagnostic),
@@ -43,14 +41,13 @@ class DiagnosticsProviderImpl {
43
41
  .map(mapRange(fragment, document))
44
42
  .filter((diag) => {
45
43
  return (hasNoNegativeLines(diag) &&
46
- isNoFalsePositiveInst(diag) &&
47
44
  isNoJSXImplicitRuntimeWarning(diag) &&
48
45
  isNoJSXMustHaveOneParent(diag) &&
49
- isNoCantUseJSX(diag) &&
50
46
  isNoCantEndWithTS(diag) &&
51
47
  isNoSpreadExpected(diag) &&
52
48
  isNoCantResolveJSONModule(diag) &&
53
- isNoMarkdownBlockQuoteWithinMarkdown(sourceFile, markdownBoundaries, diag));
49
+ isNoCantReturnOutsideFunction(diag) &&
50
+ isNoJsxCannotHaveMultipleAttrsError(diag));
54
51
  })
55
52
  .map(enhanceIfNecessary);
56
53
  }
@@ -64,7 +61,7 @@ class DiagnosticsProviderImpl {
64
61
  if (!sourceFile) {
65
62
  return boundaries;
66
63
  }
67
- function findScript(parent) {
64
+ function findTags(parent) {
68
65
  typescript_1.default.forEachChild(parent, (node) => {
69
66
  if (typescript_1.default.isJsxElement(node)) {
70
67
  let tagName = node.openingElement.tagName.getText();
@@ -80,10 +77,10 @@ class DiagnosticsProviderImpl {
80
77
  }
81
78
  }
82
79
  }
83
- findScript(node);
80
+ findTags(node);
84
81
  });
85
82
  }
86
- findScript(sourceFile);
83
+ findTags(sourceFile);
87
84
  return boundaries;
88
85
  }
89
86
  }
@@ -116,33 +113,55 @@ function mapRange(fragment, _document) {
116
113
  function hasNoNegativeLines(diagnostic) {
117
114
  return diagnostic.range.start.line >= 0 && diagnostic.range.end.line >= 0;
118
115
  }
119
- function isNoFalsePositive() {
120
- return (diagnostic) => {
121
- return isNoJsxCannotHaveMultipleAttrsError(diagnostic);
122
- };
123
- }
124
116
  /**
125
- * Jsx cannot have multiple attributes with same name,
126
- * but that's allowed for svelte
117
+ * Astro allows multiple attributes to have the same name
127
118
  */
128
119
  function isNoJsxCannotHaveMultipleAttrsError(diagnostic) {
129
120
  return diagnostic.code !== 17001;
130
121
  }
131
- function isNoJSXImplicitRuntimeWarning(diagnostic) {
132
- return diagnostic.code !== 7016 && diagnostic.code !== 2792;
133
- }
122
+ /** Astro allows component with multiple root elements */
134
123
  function isNoJSXMustHaveOneParent(diagnostic) {
135
124
  return diagnostic.code !== 2657;
136
125
  }
137
- function isNoCantUseJSX(diagnostic) {
138
- return diagnostic.code !== 17004 && diagnostic.code !== 6142;
139
- }
126
+ /** Astro allows `.ts` ending for imports, unlike TypeScript */
140
127
  function isNoCantEndWithTS(diagnostic) {
141
128
  return diagnostic.code !== 2691;
142
129
  }
143
130
  function isNoSpreadExpected(diagnostic) {
144
131
  return diagnostic.code !== 1005;
145
132
  }
133
+ function isNoJSXImplicitRuntimeWarning(diagnostic) {
134
+ return diagnostic.code !== 7016 && diagnostic.code !== 2792;
135
+ }
136
+ /**
137
+ * Ignore "Can't return outside of function body"
138
+ * This is technically a valid diagnostic, but due to how we format our TSX, the frontmatter is at top-level so we have
139
+ * to ignore this. It wasn't a problem before because users didn't need to return things but they can now with SSR
140
+ */
141
+ function isNoCantReturnOutsideFunction(diagnostic) {
142
+ return diagnostic.code !== 1108;
143
+ }
144
+ /**
145
+ * Astro allows users to import JSON modules
146
+ */
147
+ function isNoCantResolveJSONModule(diagnostic) {
148
+ return diagnostic.code !== 2732;
149
+ }
150
+ /**
151
+ * Some diagnostics have JSX-specific nomenclature or unclear description. Enhance them for more clarity.
152
+ */
153
+ function enhanceIfNecessary(diagnostic) {
154
+ if (diagnostic.code === 2322) {
155
+ // For the rare case where an user might try to put a client directive on something that is not a component
156
+ if (diagnostic.message.includes("Property 'client:") && diagnostic.message.includes("to type 'HTMLProps")) {
157
+ return {
158
+ ...diagnostic,
159
+ message: 'Client directives are only available on framework components',
160
+ };
161
+ }
162
+ }
163
+ return diagnostic;
164
+ }
146
165
  function isWithinBoundaries(boundaries, start) {
147
166
  for (let [bstart, bend] of boundaries) {
148
167
  if (start > bstart && start < bend) {
@@ -163,39 +182,6 @@ function diagnosticIsWithinBoundaries(sourceFile, boundaries, diagnostic) {
163
182
  let pos = typescript_1.default.getPositionOfLineAndCharacter(sourceFile, startRange.line, startRange.character);
164
183
  return isWithinBoundaries(boundaries, pos);
165
184
  }
166
- function isNoWithinScript(boundaries, diagnostic) {
185
+ function isNoWithinBoundary(boundaries, diagnostic) {
167
186
  return !diagnosticIsWithinBoundaries(undefined, boundaries, diagnostic);
168
187
  }
169
- /**
170
- * This allows us to have JSON module imports.
171
- */
172
- function isNoCantResolveJSONModule(diagnostic) {
173
- return diagnostic.code !== 2732;
174
- }
175
- /**
176
- * This is for using > within a markdown component like:
177
- * <Markdown>
178
- * > Blockquote here.
179
- * </Markdown>
180
- */
181
- function isNoMarkdownBlockQuoteWithinMarkdown(sourceFile, boundaries, diagnostic) {
182
- if (diagnostic.code !== 1382) {
183
- return true;
184
- }
185
- return !diagnosticIsWithinBoundaries(sourceFile, boundaries, diagnostic);
186
- }
187
- /**
188
- * Some diagnostics have JSX-specific nomenclature. Enhance them for more clarity.
189
- */
190
- function enhanceIfNecessary(diagnostic) {
191
- if (diagnostic.code === 2322) {
192
- // For the rare case where an user might try to put a client directive on something that is not a component
193
- if (diagnostic.message.includes("Property 'client:") && diagnostic.message.includes("to type 'HTMLProps")) {
194
- return {
195
- ...diagnostic,
196
- message: 'Client directives are only available on framework components',
197
- };
198
- }
199
- }
200
- return diagnostic;
201
- }
@@ -20,17 +20,20 @@ class DocumentSymbolsProviderImpl {
20
20
  }
21
21
  const symbols = [];
22
22
  this.collectSymbols(navTree, fragment, undefined, (symbol) => symbols.push(symbol));
23
+ const originalContainerName = symbols[0].name;
23
24
  const result = [];
24
25
  // Add a "Frontmatter" namespace for the frontmatter if we have a closed one
25
26
  if (document.astroMeta.frontmatter.state === 'closed') {
26
- result.push(vscode_languageserver_types_1.SymbolInformation.create('Frontmatter', vscode_languageserver_types_1.SymbolKind.Namespace, vscode_languageserver_types_1.Range.create(document.positionAt(document.astroMeta.frontmatter.startOffset), document.positionAt(document.astroMeta.frontmatter.endOffset))));
27
+ result.push(vscode_languageserver_types_1.SymbolInformation.create('Frontmatter', vscode_languageserver_types_1.SymbolKind.Namespace, vscode_languageserver_types_1.Range.create(document.positionAt(document.astroMeta.frontmatter.startOffset), document.positionAt(document.astroMeta.frontmatter.endOffset)), document.getURL()));
27
28
  }
28
29
  // Add a "Template" namespace for everything under the frontmatter
29
- result.push(vscode_languageserver_types_1.SymbolInformation.create('Template', vscode_languageserver_types_1.SymbolKind.Namespace, vscode_languageserver_types_1.Range.create(document.positionAt((_a = document.astroMeta.frontmatter.endOffset) !== null && _a !== void 0 ? _a : 0), document.positionAt(document.getTextLength()))));
30
+ result.push(vscode_languageserver_types_1.SymbolInformation.create('Template', vscode_languageserver_types_1.SymbolKind.Namespace, vscode_languageserver_types_1.Range.create(document.positionAt((_a = document.astroMeta.frontmatter.endOffset) !== null && _a !== void 0 ? _a : 0), document.positionAt(document.getTextLength())), document.getURL()));
30
31
  for (let symbol of symbols.splice(1)) {
31
32
  symbol = (0, documents_1.mapSymbolInformationToOriginal)(fragment, symbol);
32
33
  if (document.offsetAt(symbol.location.range.end) >= ((_b = document.astroMeta.content.firstNonWhitespaceOffset) !== null && _b !== void 0 ? _b : 0)) {
33
- symbol.containerName = 'Template';
34
+ if (symbol.containerName === originalContainerName) {
35
+ symbol.containerName = 'Template';
36
+ }
34
37
  // For some reason, it seems like TypeScript thinks that the "class" attribute is a real class, weird
35
38
  if (symbol.kind === vscode_languageserver_types_1.SymbolKind.Class && symbol.name === '<class>') {
36
39
  const node = document.html.findNodeAt(document.offsetAt(symbol.location.range.start));
@@ -39,8 +42,8 @@ class DocumentSymbolsProviderImpl {
39
42
  }
40
43
  }
41
44
  }
42
- // Remove the default function detected in our TSX output
43
- if (symbol.kind === vscode_languageserver_types_1.SymbolKind.Function && symbol.name == 'default') {
45
+ // Remove the exported function in our TSX output from the symbols
46
+ if (document.offsetAt(symbol.location.range.start) >= document.getTextLength()) {
44
47
  continue;
45
48
  }
46
49
  result.push(symbol);
@@ -0,0 +1,9 @@
1
+ import { FoldingRange } from 'vscode-languageserver';
2
+ import { AstroDocument } from '../../../core/documents';
3
+ import { FoldingRangesProvider } from '../../interfaces';
4
+ import { LanguageServiceManager } from '../LanguageServiceManager';
5
+ export declare class FoldingRangesProviderImpl implements FoldingRangesProvider {
6
+ private readonly languageServiceManager;
7
+ constructor(languageServiceManager: LanguageServiceManager);
8
+ getFoldingRanges(document: AstroDocument): Promise<FoldingRange[] | null>;
9
+ }
@@ -0,0 +1,64 @@
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.FoldingRangesProviderImpl = void 0;
7
+ const typescript_1 = __importDefault(require("typescript"));
8
+ const vscode_languageserver_1 = require("vscode-languageserver");
9
+ const utils_1 = require("../utils");
10
+ class FoldingRangesProviderImpl {
11
+ constructor(languageServiceManager) {
12
+ this.languageServiceManager = languageServiceManager;
13
+ }
14
+ async getFoldingRanges(document) {
15
+ const html = document.html;
16
+ const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
17
+ const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
18
+ const outliningSpans = lang.getOutliningSpans(filePath);
19
+ const foldingRanges = [];
20
+ for (const span of outliningSpans) {
21
+ const node = html.findNodeAt(span.textSpan.start);
22
+ // Due to how our TSX output transform those tags into function calls or template literals
23
+ // 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
+ }
27
+ const start = document.positionAt(span.textSpan.start);
28
+ const end = adjustFoldingEnd(start, document.positionAt(span.textSpan.start + span.textSpan.length), document);
29
+ // When using this method for generating folding ranges, TypeScript tend to return some
30
+ // one line / one character ones that we should be able to safely ignore
31
+ if (start.line === end.line && start.character === end.character) {
32
+ continue;
33
+ }
34
+ foldingRanges.push(vscode_languageserver_1.FoldingRange.create(start.line, end.line, start.character, end.character, transformFoldingRangeKind(span.kind)));
35
+ }
36
+ return foldingRanges;
37
+ }
38
+ }
39
+ exports.FoldingRangesProviderImpl = FoldingRangesProviderImpl;
40
+ function transformFoldingRangeKind(tsKind) {
41
+ switch (tsKind) {
42
+ case typescript_1.default.OutliningSpanKind.Comment:
43
+ return vscode_languageserver_1.FoldingRangeKind.Comment;
44
+ case typescript_1.default.OutliningSpanKind.Imports:
45
+ return vscode_languageserver_1.FoldingRangeKind.Imports;
46
+ case typescript_1.default.OutliningSpanKind.Region:
47
+ return vscode_languageserver_1.FoldingRangeKind.Region;
48
+ }
49
+ }
50
+ // https://github.com/microsoft/vscode/blob/bed61166fb604e519e82e4d1d1ed839bc45d65f8/extensions/typescript-language-features/src/languageFeatures/folding.ts#L61-L73
51
+ function adjustFoldingEnd(start, end, document) {
52
+ // workaround for #47240
53
+ if (end.character > 0) {
54
+ const foldEndCharacter = document.getText({
55
+ start: { line: end.line, character: end.character - 1 },
56
+ end,
57
+ });
58
+ if (['}', ']', ')', '`'].includes(foldEndCharacter)) {
59
+ const endOffset = Math.max(document.offsetAt({ line: end.line, character: 0 }) - 1, document.offsetAt(start));
60
+ return document.positionAt(endOffset);
61
+ }
62
+ }
63
+ return end;
64
+ }
@@ -22,7 +22,10 @@ class SemanticTokensProviderImpl {
22
22
  const start = range ? fragment.offsetAt(fragment.getGeneratedPosition(range.start)) : 0;
23
23
  const { spans } = lang.getEncodedSemanticClassifications(filePath, {
24
24
  start,
25
- length: range ? fragment.offsetAt(fragment.getGeneratedPosition(range.end)) - start : fragment.text.length,
25
+ length: range
26
+ ? fragment.offsetAt(fragment.getGeneratedPosition(range.end)) - start
27
+ : // We don't want tokens for things added by astro2tsx
28
+ fragment.text.lastIndexOf('export default function ') || fragment.text.length,
26
29
  }, typescript_1.default.SemanticClassificationFormat.TwentyTwenty);
27
30
  const tokens = [];
28
31
  let i = 0;