@astrojs/language-server 0.13.4 → 0.16.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 (43) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/check.js +1 -2
  3. package/dist/core/documents/DocumentMapper.js +2 -4
  4. package/dist/core/documents/parseAstro.js +1 -1
  5. package/dist/core/documents/utils.d.ts +5 -0
  6. package/dist/core/documents/utils.js +18 -5
  7. package/dist/plugins/PluginHost.d.ts +3 -2
  8. package/dist/plugins/PluginHost.js +37 -10
  9. package/dist/plugins/astro/AstroPlugin.js +1 -1
  10. package/dist/plugins/astro/features/CompletionsProvider.js +30 -15
  11. package/dist/plugins/css/CSSPlugin.js +20 -4
  12. package/dist/plugins/html/features/astro-attributes.js +43 -27
  13. package/dist/plugins/interfaces.d.ts +2 -2
  14. package/dist/plugins/typescript/LanguageServiceManager.js +1 -1
  15. package/dist/plugins/typescript/TypeScriptPlugin.d.ts +8 -4
  16. package/dist/plugins/typescript/TypeScriptPlugin.js +18 -5
  17. package/dist/plugins/typescript/astro-sys.js +3 -5
  18. package/dist/plugins/typescript/astro2tsx.d.ts +1 -1
  19. package/dist/plugins/typescript/astro2tsx.js +12 -8
  20. package/dist/plugins/typescript/features/CodeActionsProvider.d.ts +14 -0
  21. package/dist/plugins/typescript/features/CodeActionsProvider.js +141 -0
  22. package/dist/plugins/typescript/features/CompletionsProvider.d.ts +24 -6
  23. package/dist/plugins/typescript/features/CompletionsProvider.js +262 -52
  24. package/dist/plugins/typescript/features/DiagnosticsProvider.js +45 -60
  25. package/dist/plugins/typescript/features/DocumentSymbolsProvider.js +5 -6
  26. package/dist/plugins/typescript/features/FoldingRangesProvider.d.ts +9 -0
  27. package/dist/plugins/typescript/features/FoldingRangesProvider.js +64 -0
  28. package/dist/plugins/typescript/features/SemanticTokenProvider.js +2 -2
  29. package/dist/plugins/typescript/features/SignatureHelpProvider.js +2 -2
  30. package/dist/plugins/typescript/features/utils.d.ts +4 -0
  31. package/dist/plugins/typescript/features/utils.js +25 -3
  32. package/dist/plugins/typescript/language-service.d.ts +1 -1
  33. package/dist/plugins/typescript/language-service.js +44 -24
  34. package/dist/plugins/typescript/module-loader.js +1 -1
  35. package/dist/plugins/typescript/previewer.js +1 -1
  36. package/dist/plugins/typescript/snapshots/SnapshotManager.js +1 -1
  37. package/dist/plugins/typescript/snapshots/utils.js +27 -9
  38. package/dist/plugins/typescript/utils.d.ts +4 -0
  39. package/dist/plugins/typescript/utils.js +29 -1
  40. package/dist/server.js +44 -7
  41. package/dist/utils.d.ts +20 -0
  42. package/dist/utils.js +72 -3
  43. package/package.json +3 -3
@@ -15,7 +15,7 @@ class SemanticTokensProviderImpl {
15
15
  async getSemanticTokens(document, range, cancellationToken) {
16
16
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
17
17
  const fragment = (await tsDoc.createFragment());
18
- if (cancellationToken === null || cancellationToken === void 0 ? void 0 : cancellationToken.isCancellationRequested) {
18
+ if (cancellationToken?.isCancellationRequested) {
19
19
  return null;
20
20
  }
21
21
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
@@ -25,7 +25,7 @@ class SemanticTokensProviderImpl {
25
25
  length: range
26
26
  ? fragment.offsetAt(fragment.getGeneratedPosition(range.end)) - start
27
27
  : // We don't want tokens for things added by astro2tsx
28
- fragment.text.lastIndexOf('export default function (_props:') || fragment.text.length,
28
+ fragment.text.lastIndexOf('export default function ') || fragment.text.length,
29
29
  }, typescript_1.default.SemanticClassificationFormat.TwentyTwenty);
30
30
  const tokens = [];
31
31
  let i = 0;
@@ -15,7 +15,7 @@ class SignatureHelpProviderImpl {
15
15
  async getSignatureHelp(document, position, context, cancellationToken) {
16
16
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
17
17
  const fragment = await tsDoc.createFragment();
18
- if (cancellationToken === null || cancellationToken === void 0 ? void 0 : cancellationToken.isCancellationRequested) {
18
+ if (cancellationToken?.isCancellationRequested) {
19
19
  return null;
20
20
  }
21
21
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
@@ -44,7 +44,7 @@ class SignatureHelpProviderImpl {
44
44
  * adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L103
45
45
  */
46
46
  toTsTriggerReason(context) {
47
- switch (context === null || context === void 0 ? void 0 : context.triggerKind) {
47
+ switch (context?.triggerKind) {
48
48
  case vscode_languageserver_1.SignatureHelpTriggerKind.TriggerCharacter:
49
49
  if (context.triggerCharacter) {
50
50
  if (this.isReTrigger(context.isRetrigger, context.triggerCharacter)) {
@@ -1,5 +1,8 @@
1
1
  import type { SnapshotFragment, DocumentSnapshot } from '../snapshots/DocumentSnapshot';
2
2
  import type { LanguageServiceManager } from '../LanguageServiceManager';
3
+ import { Position } from 'vscode-languageserver';
4
+ import ts from 'typescript';
5
+ export declare function isPartOfImportStatement(text: string, position: Position): boolean;
3
6
  export declare class SnapshotFragmentMap {
4
7
  private languageServiceManager;
5
8
  private map;
@@ -19,3 +22,4 @@ export declare class SnapshotFragmentMap {
19
22
  }>;
20
23
  retrieveFragment(fileName: string): Promise<SnapshotFragment>;
21
24
  }
25
+ export declare function findContainingNode<T extends ts.Node>(node: ts.Node, textSpan: ts.TextSpan, predicate: (node: ts.Node) => node is T): T | undefined;
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SnapshotFragmentMap = void 0;
3
+ exports.findContainingNode = exports.SnapshotFragmentMap = exports.isPartOfImportStatement = void 0;
4
+ const documents_1 = require("../../../core/documents");
5
+ function isPartOfImportStatement(text, position) {
6
+ const line = (0, documents_1.getLineAtPosition)(position, text);
7
+ return /\s*from\s+["'][^"']*/.test(line.slice(0, position.character));
8
+ }
9
+ exports.isPartOfImportStatement = isPartOfImportStatement;
4
10
  class SnapshotFragmentMap {
5
11
  constructor(languageServiceManager) {
6
12
  this.languageServiceManager = languageServiceManager;
@@ -13,8 +19,7 @@ class SnapshotFragmentMap {
13
19
  return this.map.get(fileName);
14
20
  }
15
21
  getFragment(fileName) {
16
- var _a;
17
- return (_a = this.map.get(fileName)) === null || _a === void 0 ? void 0 : _a.fragment;
22
+ return this.map.get(fileName)?.fragment;
18
23
  }
19
24
  async retrieve(fileName) {
20
25
  let snapshotFragment = this.get(fileName);
@@ -31,3 +36,20 @@ class SnapshotFragmentMap {
31
36
  }
32
37
  }
33
38
  exports.SnapshotFragmentMap = SnapshotFragmentMap;
39
+ function findContainingNode(node, textSpan, predicate) {
40
+ const children = node.getChildren();
41
+ const end = textSpan.start + textSpan.length;
42
+ for (const child of children) {
43
+ if (!(child.getStart() <= textSpan.start && child.getEnd() >= end)) {
44
+ continue;
45
+ }
46
+ if (predicate(child)) {
47
+ return child;
48
+ }
49
+ const foundInChildren = findContainingNode(child, textSpan, predicate);
50
+ if (foundInChildren) {
51
+ return foundInChildren;
52
+ }
53
+ }
54
+ }
55
+ exports.findContainingNode = findContainingNode;
@@ -35,4 +35,4 @@ export declare function forAllLanguageServices(cb: (service: LanguageServiceCont
35
35
  * @param tsconfigPath has to be absolute
36
36
  * @param docContext
37
37
  */
38
- export declare function getLanguageServiceForTsconfig(tsconfigPath: string, docContext: LanguageServiceDocumentContext): Promise<LanguageServiceContainer>;
38
+ export declare function getLanguageServiceForTsconfig(tsconfigPath: string, docContext: LanguageServiceDocumentContext, workspaceUris: string[]): Promise<LanguageServiceContainer>;
@@ -37,7 +37,7 @@ const DocumentSnapshotUtils = __importStar(require("./snapshots/utils"));
37
37
  const services = new Map();
38
38
  async function getLanguageService(path, workspaceUris, docContext) {
39
39
  const tsconfigPath = (0, utils_2.findTsConfigPath)(path, workspaceUris);
40
- return getLanguageServiceForTsconfig(tsconfigPath, docContext);
40
+ return getLanguageServiceForTsconfig(tsconfigPath, docContext, workspaceUris);
41
41
  }
42
42
  exports.getLanguageService = getLanguageService;
43
43
  async function forAllLanguageServices(cb) {
@@ -50,34 +50,44 @@ exports.forAllLanguageServices = forAllLanguageServices;
50
50
  * @param tsconfigPath has to be absolute
51
51
  * @param docContext
52
52
  */
53
- async function getLanguageServiceForTsconfig(tsconfigPath, docContext) {
53
+ async function getLanguageServiceForTsconfig(tsconfigPath, docContext, workspaceUris) {
54
54
  let service;
55
55
  if (services.has(tsconfigPath)) {
56
56
  service = await services.get(tsconfigPath);
57
57
  }
58
58
  else {
59
- const newService = createLanguageService(tsconfigPath, docContext);
59
+ const newService = createLanguageService(tsconfigPath, docContext, workspaceUris);
60
60
  services.set(tsconfigPath, newService);
61
61
  service = await newService;
62
62
  }
63
63
  return service;
64
64
  }
65
65
  exports.getLanguageServiceForTsconfig = getLanguageServiceForTsconfig;
66
- async function createLanguageService(tsconfigPath, docContext) {
67
- const workspaceRoot = tsconfigPath ? (0, path_1.dirname)(tsconfigPath) : '';
66
+ async function createLanguageService(tsconfigPath, docContext, workspaceUris) {
67
+ const tsconfigRoot = tsconfigPath ? (0, path_1.dirname)(tsconfigPath) : process.cwd();
68
+ const workspacePaths = workspaceUris.map((uri) => (0, utils_1.urlToPath)(uri));
69
+ const workspacePath = workspacePaths.find((uri) => tsconfigRoot.startsWith(uri)) || workspacePaths[0];
70
+ const astroVersion = (0, utils_1.getUserAstroVersion)(workspacePath);
68
71
  // `raw` here represent the tsconfig merged with any extended config
69
72
  const { compilerOptions, fileNames: files, raw: fullConfig } = getParsedTSConfig();
70
73
  let projectVersion = 0;
71
- const snapshotManager = new SnapshotManager_1.SnapshotManager(docContext.globalSnapshotManager, files, fullConfig, workspaceRoot || process.cwd());
74
+ const snapshotManager = new SnapshotManager_1.SnapshotManager(docContext.globalSnapshotManager, files, fullConfig, tsconfigRoot || process.cwd());
72
75
  const astroModuleLoader = (0, module_loader_1.createAstroModuleLoader)(getScriptSnapshot, compilerOptions);
73
- let languageServerDirectory;
74
- try {
75
- languageServerDirectory = (0, path_1.dirname)(require.resolve('@astrojs/language-server'));
76
- }
77
- catch (e) {
78
- languageServerDirectory = __dirname;
76
+ const scriptFileNames = [];
77
+ // Before Astro 1.0, JSX definitions were inside of the language-server instead of inside Astro
78
+ // TODO: Remove this and astro-jsx.d.ts in types when we consider having support for Astro < 1.0 unnecessary
79
+ if (astroVersion.major === 0 || astroVersion.full === '1.0.0-beta.0') {
80
+ let languageServerDirectory;
81
+ try {
82
+ languageServerDirectory = (0, path_1.dirname)(require.resolve('@astrojs/language-server'));
83
+ }
84
+ catch (e) {
85
+ languageServerDirectory = __dirname;
86
+ }
87
+ const astroTSXFile = typescript_1.default.sys.resolvePath((0, path_1.resolve)(languageServerDirectory, '../types/astro-jsx.d.ts'));
88
+ scriptFileNames.push(astroTSXFile);
89
+ console.warn("Version lower than 1.0 detected, using internal types instead of Astro's");
79
90
  }
80
- const astroTSXFile = typescript_1.default.sys.resolvePath((0, path_1.resolve)(languageServerDirectory, '../types/astro-jsx.d.ts'));
81
91
  const host = {
82
92
  getNewLine: () => typescript_1.default.sys.newLine,
83
93
  useCaseSensitiveFileNames: () => typescript_1.default.sys.useCaseSensitiveFileNames,
@@ -87,10 +97,10 @@ async function createLanguageService(tsconfigPath, docContext) {
87
97
  fileExists: astroModuleLoader.fileExists,
88
98
  readDirectory: astroModuleLoader.readDirectory,
89
99
  getCompilationSettings: () => compilerOptions,
90
- getCurrentDirectory: () => workspaceRoot,
100
+ getCurrentDirectory: () => tsconfigRoot,
91
101
  getDefaultLibFileName: typescript_1.default.getDefaultLibFilePath,
92
102
  getProjectVersion: () => projectVersion.toString(),
93
- getScriptFileNames: () => Array.from(new Set([...snapshotManager.getProjectFileNames(), ...snapshotManager.getFileNames(), astroTSXFile])),
103
+ getScriptFileNames: () => Array.from(new Set([...snapshotManager.getProjectFileNames(), ...snapshotManager.getFileNames(), ...scriptFileNames])),
94
104
  getScriptSnapshot,
95
105
  getScriptVersion: (fileName) => getScriptSnapshot(fileName).version.toString(),
96
106
  };
@@ -122,7 +132,7 @@ async function createLanguageService(tsconfigPath, docContext) {
122
132
  function updateSnapshotFromDocument(document) {
123
133
  const filePath = document.getFilePath() || '';
124
134
  const prevSnapshot = snapshotManager.get(filePath);
125
- if ((prevSnapshot === null || prevSnapshot === void 0 ? void 0 : prevSnapshot.version) === document.version) {
135
+ if (prevSnapshot?.version === document.version) {
126
136
  return prevSnapshot;
127
137
  }
128
138
  if (!prevSnapshot) {
@@ -176,17 +186,23 @@ async function createLanguageService(tsconfigPath, docContext) {
176
186
  snapshotManager.updateNonAstroFile(fileName, changes);
177
187
  }
178
188
  function getParsedTSConfig() {
179
- var _a, _b, _c;
180
189
  let configJson = (tsconfigPath && typescript_1.default.readConfigFile(tsconfigPath, typescript_1.default.sys.readFile).config) || {};
181
190
  // If our user has types in their config but it doesn't include the types needed for Astro, add them to the config
182
- if (((_a = configJson.compilerOptions) === null || _a === void 0 ? void 0 : _a.types) && !((_b = configJson.compilerOptions) === null || _b === void 0 ? void 0 : _b.types.includes('astro/env'))) {
183
- configJson.compilerOptions.types.push('astro/env');
191
+ if (configJson.compilerOptions?.types) {
192
+ if (!configJson.compilerOptions?.types.includes('astro/env')) {
193
+ configJson.compilerOptions.types.push('astro/env');
194
+ }
195
+ if (astroVersion.major >= 1 &&
196
+ astroVersion.full !== '1.0.0-beta.0' &&
197
+ !configJson.compilerOptions?.types.includes('astro/astro-jsx')) {
198
+ configJson.compilerOptions.types.push('astro/astro-jsx');
199
+ }
184
200
  }
185
- configJson.compilerOptions = Object.assign(getDefaultCompilerOptions(), configJson.compilerOptions);
201
+ configJson.compilerOptions = Object.assign(getDefaultCompilerOptions(astroVersion), configJson.compilerOptions);
186
202
  // Delete include so that .astro files don't get mistakenly excluded by the user
187
203
  delete configJson.include;
188
204
  // If the user supplied exclude, let's use theirs otherwise, use ours
189
- (_c = configJson.exclude) !== null && _c !== void 0 ? _c : (configJson.exclude = getDefaultExclude());
205
+ configJson.exclude ?? (configJson.exclude = getDefaultExclude());
190
206
  // Everything here will always, unconditionally, be in the resulting config
191
207
  const forcedCompilerOptions = {
192
208
  noEmit: true,
@@ -200,7 +216,7 @@ async function createLanguageService(tsconfigPath, docContext) {
200
216
  target: typescript_1.default.ScriptTarget.ESNext,
201
217
  moduleResolution: typescript_1.default.ModuleResolutionKind.NodeJs,
202
218
  };
203
- const project = typescript_1.default.parseJsonConfigFileContent(configJson, typescript_1.default.sys, workspaceRoot, forcedCompilerOptions, tsconfigPath, undefined, [
219
+ const project = typescript_1.default.parseJsonConfigFileContent(configJson, typescript_1.default.sys, tsconfigRoot, forcedCompilerOptions, tsconfigPath, undefined, [
204
220
  { extension: '.vue', isMixedContent: true, scriptKind: typescript_1.default.ScriptKind.Deferred },
205
221
  { extension: '.svelte', isMixedContent: true, scriptKind: typescript_1.default.ScriptKind.Deferred },
206
222
  { extension: '.astro', isMixedContent: true, scriptKind: typescript_1.default.ScriptKind.Deferred },
@@ -218,11 +234,15 @@ async function createLanguageService(tsconfigPath, docContext) {
218
234
  /**
219
235
  * Default configuration used as a base and when the user doesn't have any
220
236
  */
221
- function getDefaultCompilerOptions() {
237
+ function getDefaultCompilerOptions(astroVersion) {
238
+ const types = ['astro/env'];
239
+ if (astroVersion.major >= 1 && astroVersion.full !== '1.0.0-beta.0') {
240
+ types.push('astro/astro-jsx');
241
+ }
222
242
  return {
223
243
  maxNodeModuleJsDepth: 2,
224
244
  allowSyntheticDefaultImports: true,
225
- types: ['astro/env'],
245
+ types: types,
226
246
  };
227
247
  }
228
248
  function getDefaultExclude() {
@@ -39,7 +39,7 @@ class ModuleResolutionCache {
39
39
  */
40
40
  delete(resolvedModuleName) {
41
41
  this.cache.forEach((val, key) => {
42
- if ((val === null || val === void 0 ? void 0 : val.resolvedFileName) === resolvedModuleName) {
42
+ if (val?.resolvedFileName === resolvedModuleName) {
43
43
  this.cache.delete(key);
44
44
  }
45
45
  });
@@ -72,7 +72,7 @@ function getTagBodyText(tag) {
72
72
  function getTagDocumentation(tag) {
73
73
  function getWithType() {
74
74
  const body = (typescript_1.default.displayPartsToString(tag.text) || '').split(/^(\S+)\s*-?\s*/);
75
- if ((body === null || body === void 0 ? void 0 : body.length) === 3) {
75
+ if (body?.length === 3) {
76
76
  const param = body[1];
77
77
  const doc = body[2];
78
78
  const label = `*@${tag.name}* \`${param}\``;
@@ -129,7 +129,7 @@ class SnapshotManager {
129
129
  const { include, exclude } = this.fileSpec;
130
130
  // Since we default to not include anything,
131
131
  // just don't waste time on this
132
- if ((include === null || include === void 0 ? void 0 : include.length) === 0) {
132
+ if (include?.length === 0) {
133
133
  return;
134
134
  }
135
135
  const projectFiles = typescript_1.default.sys
@@ -6,12 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createFromFrameworkFilePath = exports.createFromAstroFilePath = exports.createFromTSFilePath = exports.createFromNonAstroFilePath = exports.createFromFilePath = exports.createFromDocument = void 0;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  const astro2tsx_1 = __importDefault(require("../astro2tsx"));
9
+ const vscode_uri_1 = require("vscode-uri");
9
10
  const utils_1 = require("../utils");
10
11
  const DocumentSnapshot_1 = require("./DocumentSnapshot");
11
12
  const svelte_language_integration_1 = require("@astrojs/svelte-language-integration");
13
+ const utils_2 = require("../../../utils");
12
14
  // Utilities to create Snapshots from different contexts
13
15
  function createFromDocument(document) {
14
- const { code } = (0, astro2tsx_1.default)(document.getText());
16
+ const { code } = (0, astro2tsx_1.default)(document.getText(), classNameFromFilename(document.getURL()));
15
17
  return new DocumentSnapshot_1.AstroSnapshot(document, code, typescript_1.default.ScriptKind.TSX);
16
18
  }
17
19
  exports.createFromDocument = createFromDocument;
@@ -53,8 +55,7 @@ exports.createFromNonAstroFilePath = createFromNonAstroFilePath;
53
55
  * @param options options that apply in case it's a svelte file
54
56
  */
55
57
  function createFromTSFilePath(filePath) {
56
- var _a;
57
- const originalText = (_a = typescript_1.default.sys.readFile(filePath)) !== null && _a !== void 0 ? _a : '';
58
+ const originalText = typescript_1.default.sys.readFile(filePath) ?? '';
58
59
  return new DocumentSnapshot_1.TypeScriptDocumentSnapshot(0, filePath, originalText);
59
60
  }
60
61
  exports.createFromTSFilePath = createFromTSFilePath;
@@ -64,21 +65,38 @@ exports.createFromTSFilePath = createFromTSFilePath;
64
65
  * @param createDocument function that is used to create a document
65
66
  */
66
67
  function createFromAstroFilePath(filePath, createDocument) {
67
- var _a;
68
- const originalText = (_a = typescript_1.default.sys.readFile(filePath)) !== null && _a !== void 0 ? _a : '';
68
+ const originalText = typescript_1.default.sys.readFile(filePath) ?? '';
69
69
  return createFromDocument(createDocument(filePath, originalText));
70
70
  }
71
71
  exports.createFromAstroFilePath = createFromAstroFilePath;
72
72
  function createFromFrameworkFilePath(filePath, framework) {
73
- var _a;
74
- const originalText = (_a = typescript_1.default.sys.readFile(filePath)) !== null && _a !== void 0 ? _a : '';
73
+ const className = classNameFromFilename(filePath);
74
+ const originalText = typescript_1.default.sys.readFile(filePath) ?? '';
75
75
  let code = '';
76
76
  if (framework === 'svelte') {
77
- code = (0, svelte_language_integration_1.toTSX)(originalText);
77
+ code = (0, svelte_language_integration_1.toTSX)(originalText, className);
78
78
  }
79
79
  else {
80
- code = 'export default function(props: Record<string, any>): any {<div></div>}';
80
+ code = `export default function ${className}__AstroComponent_(props: Record<string, any>): any {}`;
81
81
  }
82
82
  return new DocumentSnapshot_1.TypeScriptDocumentSnapshot(0, filePath, code, typescript_1.default.ScriptKind.TSX);
83
83
  }
84
84
  exports.createFromFrameworkFilePath = createFromFrameworkFilePath;
85
+ function classNameFromFilename(filename) {
86
+ const url = vscode_uri_1.URI.parse(filename);
87
+ const withoutExtensions = vscode_uri_1.Utils.basename(url).slice(0, -vscode_uri_1.Utils.extname(url).length);
88
+ const withoutInvalidCharacters = withoutExtensions
89
+ .split('')
90
+ // Although "-" is invalid, we leave it in, pascal-case-handling will throw it out later
91
+ .filter((char) => /[A-Za-z$_\d-]/.test(char))
92
+ .join('');
93
+ const firstValidCharIdx = withoutInvalidCharacters
94
+ .split('')
95
+ // Although _ and $ are valid first characters for classes, they are invalid first characters
96
+ // for tag names. For a better import autocompletion experience, we therefore throw them out.
97
+ .findIndex((char) => /[A-Za-z]/.test(char));
98
+ const withoutLeadingInvalidCharacters = withoutInvalidCharacters.substr(firstValidCharIdx);
99
+ const inPascalCase = (0, utils_2.toPascalCase)(withoutLeadingInvalidCharacters);
100
+ const finalName = firstValidCharIdx === -1 ? `A${inPascalCase}` : inPascalCase;
101
+ return finalName;
102
+ }
@@ -1,5 +1,6 @@
1
1
  import ts from 'typescript';
2
2
  import { CompletionItemKind, DiagnosticSeverity, Position, Range, SymbolKind, SemanticTokensLegend } from 'vscode-languageserver';
3
+ import { AstroDocument } from '../../core/documents';
3
4
  import { SnapshotFragment } from './snapshots/DocumentSnapshot';
4
5
  export declare const enum TokenType {
5
6
  class = 0,
@@ -39,6 +40,9 @@ export declare function convertRange(document: {
39
40
  length?: number;
40
41
  }): Range;
41
42
  export declare function convertToLocationRange(defDoc: SnapshotFragment, textSpan: ts.TextSpan): Range;
43
+ export declare function ensureFrontmatterInsert(resultRange: Range, document: AstroDocument): Range;
44
+ export declare function checkEndOfFileCodeInsert(resultRange: Range, document: AstroDocument): Range;
45
+ export declare function removeAstroComponentSuffix(name: string): string;
42
46
  export declare type FrameworkExt = 'astro' | 'vue' | 'jsx' | 'tsx' | 'svelte';
43
47
  declare type FrameworkVirtualExt = 'ts' | 'tsx';
44
48
  export declare function getFrameworkFromFilePath(filePath: string): FrameworkExt;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ensureRealFilePath = exports.ensureRealAstroFilePath = exports.toRealAstroFilePath = exports.toVirtualFilePath = exports.toVirtualAstroFilePath = exports.isVirtualFilePath = exports.isVirtualSvelteFilePath = exports.isVirtualVueFilePath = exports.isVirtualAstroFilePath = exports.isFrameworkFilePath = exports.isAstroFilePath = exports.isVirtualFrameworkFilePath = exports.getFrameworkFromFilePath = exports.convertToLocationRange = exports.convertRange = exports.mapSeverity = exports.getScriptKindFromFileName = exports.isSubPath = exports.findTsConfigPath = exports.getExtensionFromScriptKind = exports.getCommitCharactersForScriptElement = exports.scriptElementKindToCompletionItemKind = exports.symbolKindFromString = exports.getSemanticTokenLegend = void 0;
6
+ exports.ensureRealFilePath = exports.ensureRealAstroFilePath = exports.toRealAstroFilePath = exports.toVirtualFilePath = exports.toVirtualAstroFilePath = exports.isVirtualFilePath = exports.isVirtualSvelteFilePath = exports.isVirtualVueFilePath = exports.isVirtualAstroFilePath = exports.isFrameworkFilePath = exports.isAstroFilePath = exports.isVirtualFrameworkFilePath = exports.getFrameworkFromFilePath = exports.removeAstroComponentSuffix = exports.checkEndOfFileCodeInsert = exports.ensureFrontmatterInsert = exports.convertToLocationRange = exports.convertRange = exports.mapSeverity = exports.getScriptKindFromFileName = exports.isSubPath = exports.findTsConfigPath = exports.getExtensionFromScriptKind = exports.getCommitCharactersForScriptElement = exports.scriptElementKindToCompletionItemKind = exports.symbolKindFromString = exports.getSemanticTokenLegend = void 0;
7
7
  const typescript_1 = __importDefault(require("typescript"));
8
8
  const path_1 = require("path");
9
9
  const utils_1 = require("../../utils");
@@ -239,6 +239,34 @@ function convertToLocationRange(defDoc, textSpan) {
239
239
  return range;
240
240
  }
241
241
  exports.convertToLocationRange = convertToLocationRange;
242
+ // Some code actions will insert code at the start of the file instead of inside our frontmatter
243
+ // We'll redirect those to the proper starting place
244
+ function ensureFrontmatterInsert(resultRange, document) {
245
+ if (document.astroMeta.frontmatter.state === 'closed') {
246
+ const position = document.positionAt(document.astroMeta.frontmatter.startOffset);
247
+ position.line += 1;
248
+ position.character = resultRange.start.character;
249
+ return vscode_languageserver_1.Range.create(position, position);
250
+ }
251
+ return resultRange;
252
+ }
253
+ exports.ensureFrontmatterInsert = ensureFrontmatterInsert;
254
+ // Some code actions ill insert code at the end of the generated TSX file, so we'll manually
255
+ // redirect it to the end of the frontmatter instead
256
+ function checkEndOfFileCodeInsert(resultRange, document) {
257
+ if (resultRange.start.line > document.lineCount) {
258
+ if (document.astroMeta.frontmatter.state === 'closed') {
259
+ const position = document.positionAt(document.astroMeta.frontmatter.endOffset);
260
+ return vscode_languageserver_1.Range.create(position, position);
261
+ }
262
+ }
263
+ return resultRange;
264
+ }
265
+ exports.checkEndOfFileCodeInsert = checkEndOfFileCodeInsert;
266
+ function removeAstroComponentSuffix(name) {
267
+ return name.replace(/(\w+)__AstroComponent_/, '$1');
268
+ }
269
+ exports.removeAstroComponentSuffix = removeAstroComponentSuffix;
242
270
  const VirtualExtension = {
243
271
  ts: 'ts',
244
272
  tsx: 'tsx',
package/dist/server.js CHANGED
@@ -36,6 +36,7 @@ const PluginHost_1 = require("./plugins/PluginHost");
36
36
  const plugins_1 = require("./plugins");
37
37
  const utils_1 = require("./utils");
38
38
  const utils_2 = require("./plugins/typescript/utils");
39
+ const CodeActionsProvider_1 = require("./plugins/typescript/features/CodeActionsProvider");
39
40
  const TagCloseRequest = new vscode.RequestType('html/tag');
40
41
  // Start the language server
41
42
  function startLanguageServer(connection) {
@@ -44,11 +45,26 @@ function startLanguageServer(connection) {
44
45
  const documentManager = new DocumentManager_1.DocumentManager();
45
46
  const pluginHost = new PluginHost_1.PluginHost(documentManager);
46
47
  connection.onInitialize((params) => {
47
- var _a, _b, _c, _d, _e, _f;
48
- const workspaceUris = (_b = (_a = params.workspaceFolders) === null || _a === void 0 ? void 0 : _a.map((folder) => folder.uri.toString())) !== null && _b !== void 0 ? _b : [(_c = params.rootUri) !== null && _c !== void 0 ? _c : ''];
48
+ const workspaceUris = params.workspaceFolders?.map((folder) => folder.uri.toString()) ?? [params.rootUri ?? ''];
49
+ workspaceUris.forEach((uri) => {
50
+ uri = (0, utils_1.urlToPath)(uri);
51
+ const astroVersion = (0, utils_1.getUserAstroVersion)(uri);
52
+ if (astroVersion.exist === false) {
53
+ connection.sendNotification(vscode_languageserver_1.ShowMessageNotification.type, {
54
+ message: `Couldn't find Astro in workspace "${uri}". Experience might be degraded. For the best experience, please make sure Astro is installed and then restart the language server`,
55
+ type: vscode_languageserver_1.MessageType.Warning,
56
+ });
57
+ }
58
+ if (astroVersion.exist && astroVersion.major === 0 && astroVersion.minor < 24 && astroVersion.patch < 5) {
59
+ connection.sendNotification(vscode_languageserver_1.ShowMessageNotification.type, {
60
+ message: `The version of Astro you're using (${astroVersion.full}) is not supported by this version of the Astro language server. Please upgrade Astro to any version higher than 0.23.4 or if using the VS Code extension, downgrade the extension to 0.8.10`,
61
+ type: vscode_languageserver_1.MessageType.Error,
62
+ });
63
+ }
64
+ });
49
65
  pluginHost.initialize({
50
- filterIncompleteCompletions: !((_d = params.initializationOptions) === null || _d === void 0 ? void 0 : _d.dontFilterIncompleteCompletions),
51
- definitionLinkSupport: !!((_f = (_e = params.capabilities.textDocument) === null || _e === void 0 ? void 0 : _e.definition) === null || _f === void 0 ? void 0 : _f.linkSupport),
66
+ filterIncompleteCompletions: !params.initializationOptions?.dontFilterIncompleteCompletions,
67
+ definitionLinkSupport: !!params.capabilities.textDocument?.definition?.linkSupport,
52
68
  });
53
69
  // Register plugins
54
70
  pluginHost.registerPlugin(new HTMLPlugin_1.HTMLPlugin(configManager));
@@ -59,14 +75,34 @@ function startLanguageServer(connection) {
59
75
  pluginHost.registerPlugin(new plugins_1.TypeScriptPlugin(documentManager, configManager, workspaceUris));
60
76
  }
61
77
  // Update language-server config with what the user supplied to us at launch
62
- configManager.updateConfig(params.initializationOptions.configuration.astro);
63
- configManager.updateEmmetConfig(params.initializationOptions.configuration.emmet);
78
+ let astroConfiguration = params.initializationOptions?.configuration?.astro;
79
+ if (astroConfiguration) {
80
+ configManager.updateConfig(astroConfiguration);
81
+ }
82
+ let emmetConfiguration = params.initializationOptions?.configuration?.emmet;
83
+ if (emmetConfiguration) {
84
+ configManager.updateEmmetConfig(emmetConfiguration);
85
+ }
64
86
  return {
65
87
  capabilities: {
66
- textDocumentSync: vscode.TextDocumentSyncKind.Incremental,
88
+ textDocumentSync: {
89
+ openClose: true,
90
+ change: vscode.TextDocumentSyncKind.Incremental,
91
+ save: {
92
+ includeText: true,
93
+ },
94
+ },
67
95
  foldingRangeProvider: true,
68
96
  definitionProvider: true,
69
97
  renameProvider: true,
98
+ codeActionProvider: {
99
+ codeActionKinds: [
100
+ vscode_languageserver_1.CodeActionKind.QuickFix,
101
+ vscode_languageserver_1.CodeActionKind.SourceOrganizeImports,
102
+ // VS Code specific
103
+ CodeActionsProvider_1.sortImportKind,
104
+ ],
105
+ },
70
106
  completionProvider: {
71
107
  resolveProvider: true,
72
108
  triggerCharacters: [
@@ -141,6 +177,7 @@ function startLanguageServer(connection) {
141
177
  connection.onHover((params) => pluginHost.doHover(params.textDocument, params.position));
142
178
  connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position));
143
179
  connection.onFoldingRanges((evt) => pluginHost.getFoldingRanges(evt.textDocument));
180
+ connection.onCodeAction((evt, cancellationToken) => pluginHost.getCodeActions(evt.textDocument, evt.range, evt.context, cancellationToken));
144
181
  connection.onCompletion(async (evt) => {
145
182
  const promise = pluginHost.getCompletions(evt.textDocument, evt.position, evt.context);
146
183
  return promise;
package/dist/utils.d.ts CHANGED
@@ -16,6 +16,14 @@ export declare function pathToUrl(path: string): string;
16
16
  * (bar or bar.astro in this example).
17
17
  */
18
18
  export declare function getLastPartOfPath(path: string): string;
19
+ /**
20
+ * Transform a string into PascalCase
21
+ */
22
+ export declare function toPascalCase(string: string): string;
23
+ /**
24
+ * Function to modify each line of a text, preserving the line break style (`\n` or `\r\n`)
25
+ */
26
+ export declare function modifyLines(text: string, replacementFn: (line: string, lineIdx: number) => string): string;
19
27
  /**
20
28
  * Return true if a specific node could be a component.
21
29
  * This is not a 100% sure test as it'll return false for any component that does not match the standard format for a component
@@ -28,6 +36,10 @@ export declare function clamp(num: number, min: number, max: number): number;
28
36
  export declare function isNotNullOrUndefined<T>(val: T | undefined | null): val is T;
29
37
  export declare function isInRange(range: Range, positionToTest: Position): boolean;
30
38
  export declare function isBeforeOrEqualToPosition(position: Position, positionToTest: Position): boolean;
39
+ /**
40
+ * Get all matches of a regexp.
41
+ */
42
+ export declare function getRegExpMatches(regex: RegExp, str: string): RegExpExecArray[];
31
43
  /**
32
44
  * Debounces a function but cancels previous invocation only if
33
45
  * a second function determines it should.
@@ -46,3 +58,11 @@ export declare function debounceSameArg<T>(fn: (arg: T) => void, shouldCancelPre
46
58
  * @param milliseconds Number of milliseconds to debounce/throttle
47
59
  */
48
60
  export declare function debounceThrottle<T extends (...args: any) => void>(fn: T, milliseconds: number): T;
61
+ export interface AstroVersion {
62
+ full: string;
63
+ major: number;
64
+ minor: number;
65
+ patch: number;
66
+ exist: boolean;
67
+ }
68
+ export declare function getUserAstroVersion(basePath: string): AstroVersion;
package/dist/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.debounceThrottle = exports.debounceSameArg = exports.isBeforeOrEqualToPosition = exports.isInRange = exports.isNotNullOrUndefined = exports.clamp = exports.flatten = exports.isPossibleComponent = exports.getLastPartOfPath = exports.pathToUrl = exports.urlToPath = exports.normalizePath = exports.normalizeUri = void 0;
3
+ exports.getUserAstroVersion = exports.debounceThrottle = exports.debounceSameArg = exports.getRegExpMatches = exports.isBeforeOrEqualToPosition = exports.isInRange = exports.isNotNullOrUndefined = exports.clamp = exports.flatten = exports.isPossibleComponent = exports.modifyLines = exports.toPascalCase = exports.getLastPartOfPath = exports.pathToUrl = exports.urlToPath = exports.normalizePath = exports.normalizeUri = void 0;
4
4
  const vscode_uri_1 = require("vscode-uri");
5
5
  /** Normalizes a document URI */
6
6
  function normalizeUri(uri) {
@@ -37,13 +37,37 @@ function getLastPartOfPath(path) {
37
37
  return path.replace(/\\/g, '/').split('/').pop() || '';
38
38
  }
39
39
  exports.getLastPartOfPath = getLastPartOfPath;
40
+ /**
41
+ * Transform a string into PascalCase
42
+ */
43
+ function toPascalCase(string) {
44
+ return `${string}`
45
+ .replace(new RegExp(/[-_]+/, 'g'), ' ')
46
+ .replace(new RegExp(/[^\w\s]/, 'g'), '')
47
+ .replace(new RegExp(/\s+(.)(\w*)/, 'g'), ($1, $2, $3) => `${$2.toUpperCase() + $3.toLowerCase()}`)
48
+ .replace(new RegExp(/\w/), (s) => s.toUpperCase());
49
+ }
50
+ exports.toPascalCase = toPascalCase;
51
+ /**
52
+ * Function to modify each line of a text, preserving the line break style (`\n` or `\r\n`)
53
+ */
54
+ function modifyLines(text, replacementFn) {
55
+ let idx = 0;
56
+ return text
57
+ .split('\r\n')
58
+ .map((l1) => l1
59
+ .split('\n')
60
+ .map((line) => replacementFn(line, idx++))
61
+ .join('\n'))
62
+ .join('\r\n');
63
+ }
64
+ exports.modifyLines = modifyLines;
40
65
  /**
41
66
  * Return true if a specific node could be a component.
42
67
  * This is not a 100% sure test as it'll return false for any component that does not match the standard format for a component
43
68
  */
44
69
  function isPossibleComponent(node) {
45
- var _a, _b;
46
- return !!((_a = node.tag) === null || _a === void 0 ? void 0 : _a[0].match(/[A-Z]/)) || !!((_b = node.tag) === null || _b === void 0 ? void 0 : _b.match(/.+[.][A-Z]/));
70
+ return !!node.tag?.[0].match(/[A-Z]/) || !!node.tag?.match(/.+[.][A-Z]/);
47
71
  }
48
72
  exports.isPossibleComponent = isPossibleComponent;
49
73
  /** Flattens an array */
@@ -69,6 +93,18 @@ function isBeforeOrEqualToPosition(position, positionToTest) {
69
93
  (positionToTest.line === position.line && positionToTest.character <= position.character));
70
94
  }
71
95
  exports.isBeforeOrEqualToPosition = isBeforeOrEqualToPosition;
96
+ /**
97
+ * Get all matches of a regexp.
98
+ */
99
+ function getRegExpMatches(regex, str) {
100
+ const matches = [];
101
+ let match;
102
+ while ((match = regex.exec(str))) {
103
+ matches.push(match);
104
+ }
105
+ return matches;
106
+ }
107
+ exports.getRegExpMatches = getRegExpMatches;
72
108
  /**
73
109
  * Debounces a function but cancels previous invocation only if
74
110
  * a second function determines it should.
@@ -117,3 +153,36 @@ function debounceThrottle(fn, milliseconds) {
117
153
  return maybeCall;
118
154
  }
119
155
  exports.debounceThrottle = debounceThrottle;
156
+ function getUserAstroVersion(basePath) {
157
+ let version = '0.0.0';
158
+ let exist = true;
159
+ try {
160
+ const astroPackageJson = require.resolve('astro/package.json', { paths: [basePath] });
161
+ version = require(astroPackageJson).version;
162
+ }
163
+ catch {
164
+ // If we couldn't find it inside the workspace's node_modules, it might means we're in the monorepo
165
+ try {
166
+ const monorepoPackageJson = require.resolve('./packages/astro/package.json', { paths: [basePath] });
167
+ version = require(monorepoPackageJson).version;
168
+ }
169
+ catch (e) {
170
+ // If we still couldn't find it, it probably just doesn't exist
171
+ exist = false;
172
+ console.error(e);
173
+ }
174
+ }
175
+ let [major, minor, patch] = version.split('.');
176
+ if (patch.includes('-')) {
177
+ const patchParts = patch.split('-');
178
+ patch = patchParts[0];
179
+ }
180
+ return {
181
+ full: version,
182
+ major: Number(major),
183
+ minor: Number(minor),
184
+ patch: Number(patch),
185
+ exist,
186
+ };
187
+ }
188
+ exports.getUserAstroVersion = getUserAstroVersion;