@astrojs/language-server 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @astrojs/language-server
2
2
 
3
+ ## 0.17.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 3ad0f65: Add support for TypeScript features inside script tags (completions, diagnostics, hover etc)
8
+
9
+ ### Patch Changes
10
+
11
+ - 2e9da14: Add support for loading props completions from .d.ts files, improve performance of props completions
12
+
3
13
  ## 0.16.1
4
14
 
5
15
  ### Patch Changes
@@ -9,6 +9,7 @@ export declare class AstroDocument extends WritableDocument {
9
9
  astroMeta: AstroMetadata;
10
10
  html: HTMLDocument;
11
11
  styleTags: TagInformation[];
12
+ scriptTags: TagInformation[];
12
13
  constructor(url: string, content: string);
13
14
  private updateDocInfo;
14
15
  setText(text: string): void;
@@ -18,6 +18,7 @@ class AstroDocument extends DocumentBase_1.WritableDocument {
18
18
  this.astroMeta = (0, parseAstro_1.parseAstro)(this.content);
19
19
  this.html = (0, parseHtml_1.parseHtml)(this.content);
20
20
  this.styleTags = (0, utils_2.extractStyleTags)(this.content, this.html);
21
+ this.scriptTags = (0, utils_2.extractScriptTags)(this.content, this.html);
21
22
  }
22
23
  setText(text) {
23
24
  this.content = text;
@@ -46,6 +46,8 @@ export declare class FragmentMapper implements DocumentMapper {
46
46
  private originalText;
47
47
  private tagInfo;
48
48
  private url;
49
+ private lineOffsetsOriginal;
50
+ private lineOffsetsGenerated;
49
51
  constructor(originalText: string, tagInfo: TagInformation, url: string);
50
52
  getOriginalPosition(generatedPosition: Position): Position;
51
53
  private offsetInParent;
@@ -45,20 +45,22 @@ class FragmentMapper {
45
45
  this.originalText = originalText;
46
46
  this.tagInfo = tagInfo;
47
47
  this.url = url;
48
+ this.lineOffsetsOriginal = (0, utils_1.getLineOffsets)(this.originalText);
49
+ this.lineOffsetsGenerated = (0, utils_1.getLineOffsets)(this.tagInfo.content);
48
50
  }
49
51
  getOriginalPosition(generatedPosition) {
50
- const parentOffset = this.offsetInParent((0, utils_1.offsetAt)(generatedPosition, this.tagInfo.content));
51
- return (0, utils_1.positionAt)(parentOffset, this.originalText);
52
+ const parentOffset = this.offsetInParent((0, utils_1.offsetAt)(generatedPosition, this.tagInfo.content, this.lineOffsetsGenerated));
53
+ return (0, utils_1.positionAt)(parentOffset, this.originalText, this.lineOffsetsOriginal);
52
54
  }
53
55
  offsetInParent(offset) {
54
56
  return this.tagInfo.start + offset;
55
57
  }
56
58
  getGeneratedPosition(originalPosition) {
57
- const fragmentOffset = (0, utils_1.offsetAt)(originalPosition, this.originalText) - this.tagInfo.start;
58
- return (0, utils_1.positionAt)(fragmentOffset, this.tagInfo.content);
59
+ const fragmentOffset = (0, utils_1.offsetAt)(originalPosition, this.originalText, this.lineOffsetsOriginal) - this.tagInfo.start;
60
+ return (0, utils_1.positionAt)(fragmentOffset, this.tagInfo.content, this.lineOffsetsGenerated);
59
61
  }
60
62
  isInGenerated(pos) {
61
- const offset = (0, utils_1.offsetAt)(pos, this.originalText);
63
+ const offset = (0, utils_1.offsetAt)(pos, this.originalText, this.lineOffsetsOriginal);
62
64
  return offset >= this.tagInfo.start && offset <= this.tagInfo.end;
63
65
  }
64
66
  getURL() {
@@ -15,6 +15,7 @@ export interface TagInformation {
15
15
  }
16
16
  export declare function walk(node: Node): Generator<Node, void, unknown>;
17
17
  export declare function extractStyleTags(source: string, html?: HTMLDocument): TagInformation[];
18
+ export declare function extractScriptTags(source: string, html?: HTMLDocument): TagInformation[];
18
19
  export declare function getLineAtPosition(position: Position, text: string): string;
19
20
  /**
20
21
  * Returns the node if offset is inside a HTML start tag
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getFirstNonWhitespaceIndex = exports.getLineOffsets = exports.offsetAt = exports.positionAt = exports.isInsideFrontmatter = exports.isInsideExpression = exports.isInTag = exports.isInComponentStartTag = exports.isComponentTag = exports.getNodeIfIsInHTMLStartTag = exports.getLineAtPosition = exports.extractStyleTags = exports.walk = void 0;
3
+ exports.getFirstNonWhitespaceIndex = exports.getLineOffsets = exports.offsetAt = exports.positionAt = exports.isInsideFrontmatter = exports.isInsideExpression = exports.isInTag = exports.isInComponentStartTag = exports.isComponentTag = exports.getNodeIfIsInHTMLStartTag = exports.getLineAtPosition = exports.extractScriptTags = exports.extractStyleTags = exports.walk = void 0;
4
4
  const vscode_languageserver_1 = require("vscode-languageserver");
5
5
  const utils_1 = require("../../utils");
6
6
  const parseHtml_1 = require("./parseHtml");
@@ -28,6 +28,13 @@ function extractTags(text, tag, html) {
28
28
  }
29
29
  }
30
30
  }
31
+ if (tag === 'script' && !matchedNodes.length && rootNodes.length) {
32
+ for (let child of walk(rootNodes[0])) {
33
+ if (child.tag === 'script') {
34
+ matchedNodes.push(child);
35
+ }
36
+ }
37
+ }
31
38
  return matchedNodes.map(transformToTagInfo);
32
39
  function transformToTagInfo(matchedNode) {
33
40
  const start = matchedNode.startTagEnd ?? matchedNode.start;
@@ -60,6 +67,14 @@ function extractStyleTags(source, html) {
60
67
  return styles;
61
68
  }
62
69
  exports.extractStyleTags = extractStyleTags;
70
+ function extractScriptTags(source, html) {
71
+ const scripts = extractTags(source, 'script', html);
72
+ if (!scripts.length) {
73
+ return [];
74
+ }
75
+ return scripts;
76
+ }
77
+ exports.extractScriptTags = extractScriptTags;
63
78
  function parseAttributes(rawAttrs) {
64
79
  const attrs = {};
65
80
  if (!rawAttrs) {
@@ -9,7 +9,7 @@ class AstroPlugin {
9
9
  this.__name = 'astro';
10
10
  this.configManager = configManager;
11
11
  this.languageServiceManager = new LanguageServiceManager_1.LanguageServiceManager(docManager, workspaceUris, configManager);
12
- this.completionProvider = new CompletionsProvider_1.CompletionsProviderImpl(docManager, this.languageServiceManager);
12
+ this.completionProvider = new CompletionsProvider_1.CompletionsProviderImpl(this.languageServiceManager);
13
13
  }
14
14
  async getCompletions(document, position, completionContext) {
15
15
  const completions = this.completionProvider.getCompletions(document, position, completionContext);
@@ -1,17 +1,16 @@
1
1
  import type { AppCompletionList, CompletionsProvider } from '../../interfaces';
2
- import type { AstroDocument, DocumentManager } from '../../../core/documents';
2
+ import type { AstroDocument } from '../../../core/documents';
3
3
  import { CompletionContext, Position } from 'vscode-languageserver';
4
4
  import { LanguageServiceManager as TypeScriptLanguageServiceManager } from '../../typescript/LanguageServiceManager';
5
5
  export declare class CompletionsProviderImpl implements CompletionsProvider {
6
- private readonly docManager;
7
6
  private readonly languageServiceManager;
7
+ private lastCompletion;
8
8
  directivesHTMLLang: import("vscode-html-languageservice").LanguageService;
9
- constructor(docManager: DocumentManager, languageServiceManager: TypeScriptLanguageServiceManager);
9
+ constructor(languageServiceManager: TypeScriptLanguageServiceManager);
10
10
  getCompletions(document: AstroDocument, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList | null>;
11
11
  private getComponentScriptCompletion;
12
- private getPropCompletions;
12
+ private getPropCompletionsAndFilePath;
13
13
  private getImportedSymbol;
14
14
  private getPropType;
15
15
  private getCompletionItemForProperty;
16
- private isAstroComponent;
17
16
  }
@@ -13,21 +13,18 @@ const vscode_html_languageservice_1 = require("vscode-html-languageservice");
13
13
  const astro_attributes_1 = require("../../html/features/astro-attributes");
14
14
  const utils_4 = require("../../html/utils");
15
15
  class CompletionsProviderImpl {
16
- constructor(docManager, languageServiceManager) {
16
+ constructor(languageServiceManager) {
17
+ this.lastCompletion = null;
17
18
  this.directivesHTMLLang = (0, vscode_html_languageservice_1.getLanguageService)({
18
19
  customDataProviders: [astro_attributes_1.astroDirectives],
19
20
  useDefaultDataProvider: false,
20
21
  });
21
- this.docManager = docManager;
22
22
  this.languageServiceManager = languageServiceManager;
23
23
  }
24
24
  async getCompletions(document, position, completionContext) {
25
- const doc = this.docManager.get(document.uri);
26
- if (!doc)
27
- return null;
28
25
  let items = [];
29
26
  if (completionContext?.triggerCharacter === '-') {
30
- const frontmatter = this.getComponentScriptCompletion(doc, position, completionContext);
27
+ const frontmatter = this.getComponentScriptCompletion(document, position);
31
28
  if (frontmatter)
32
29
  items.push(frontmatter);
33
30
  }
@@ -35,11 +32,11 @@ class CompletionsProviderImpl {
35
32
  const offset = document.offsetAt(position);
36
33
  const node = html.findNodeAt(offset);
37
34
  if ((0, utils_1.isInComponentStartTag)(html, offset) && !(0, utils_1.isInsideExpression)(document.getText(), node.start, offset)) {
38
- const props = await this.getPropCompletions(document, position, completionContext);
35
+ const { completions: props, componentFilePath } = await this.getPropCompletionsAndFilePath(document, position, completionContext);
39
36
  if (props.length) {
40
37
  items.push(...props);
41
38
  }
42
- const isAstro = await this.isAstroComponent(document, node);
39
+ const isAstro = componentFilePath?.endsWith('.astro');
43
40
  if (!isAstro) {
44
41
  const directives = (0, utils_4.removeDataAttrCompletion)(this.directivesHTMLLang.doComplete(document, position, html).items);
45
42
  items.push(...directives);
@@ -47,7 +44,7 @@ class CompletionsProviderImpl {
47
44
  }
48
45
  return vscode_languageserver_1.CompletionList.create(items, true);
49
46
  }
50
- getComponentScriptCompletion(document, position, completionContext) {
47
+ getComponentScriptCompletion(document, position) {
51
48
  const base = {
52
49
  kind: vscode_languageserver_1.CompletionItemKind.Snippet,
53
50
  label: '---',
@@ -55,7 +52,7 @@ class CompletionsProviderImpl {
55
52
  preselect: true,
56
53
  detail: 'Component script',
57
54
  insertTextFormat: vscode_languageserver_1.InsertTextFormat.Snippet,
58
- commitCharacters: ['-'],
55
+ commitCharacters: [],
59
56
  };
60
57
  const prefix = document.getLineUntilOffset(document.offsetAt(position));
61
58
  if (document.astroMeta.frontmatter.state === null) {
@@ -78,25 +75,25 @@ class CompletionsProviderImpl {
78
75
  }
79
76
  return null;
80
77
  }
81
- async getPropCompletions(document, position, completionContext) {
78
+ async getPropCompletionsAndFilePath(document, position, completionContext) {
82
79
  const offset = document.offsetAt(position);
83
80
  const html = document.html;
84
81
  const node = html.findNodeAt(offset);
85
82
  if (!(0, utils_2.isPossibleComponent)(node)) {
86
- return [];
83
+ return { completions: [], componentFilePath: null };
87
84
  }
88
85
  const inAttribute = node.start + node.tag.length < offset;
89
86
  if (!inAttribute) {
90
- return [];
87
+ return { completions: [], componentFilePath: null };
91
88
  }
92
89
  if (completionContext?.triggerCharacter === '/' || completionContext?.triggerCharacter === '>') {
93
- return [];
90
+ return { completions: [], componentFilePath: null };
94
91
  }
95
92
  // If inside of attribute value, skip.
96
93
  if (completionContext &&
97
94
  completionContext.triggerKind === vscode_languageserver_1.CompletionTriggerKind.TriggerCharacter &&
98
95
  completionContext.triggerCharacter === '"') {
99
- return [];
96
+ return { completions: [], componentFilePath: null };
100
97
  }
101
98
  const componentName = node.tag;
102
99
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
@@ -106,18 +103,34 @@ class CompletionsProviderImpl {
106
103
  const sourceFile = program?.getSourceFile(tsFilePath);
107
104
  const typeChecker = program?.getTypeChecker();
108
105
  if (!sourceFile || !typeChecker) {
109
- return [];
106
+ return { completions: [], componentFilePath: null };
110
107
  }
111
108
  // Get the import statement
112
109
  const imp = this.getImportedSymbol(sourceFile, componentName);
113
110
  const importType = imp && typeChecker.getTypeAtLocation(imp);
114
111
  if (!importType) {
115
- return [];
112
+ return { completions: [], componentFilePath: null };
113
+ }
114
+ const symbol = importType.getSymbol();
115
+ if (!symbol) {
116
+ return { completions: [], componentFilePath: null };
117
+ }
118
+ const symbolDeclaration = symbol.declarations;
119
+ if (!symbolDeclaration) {
120
+ return { completions: [], componentFilePath: null };
121
+ }
122
+ const filePath = symbolDeclaration[0].getSourceFile().fileName;
123
+ const componentSnapshot = await this.languageServiceManager.getSnapshot(filePath);
124
+ if (this.lastCompletion) {
125
+ if (this.lastCompletion.tag === componentName &&
126
+ this.lastCompletion.documentVersion == componentSnapshot.version) {
127
+ return { completions: this.lastCompletion.completions, componentFilePath: filePath };
128
+ }
116
129
  }
117
130
  // Get the component's props type
118
- const componentType = this.getPropType(importType, typeChecker);
131
+ const componentType = this.getPropType(symbolDeclaration, typeChecker);
119
132
  if (!componentType) {
120
- return [];
133
+ return { completions: [], componentFilePath: null };
121
134
  }
122
135
  let completionItems = [];
123
136
  // Add completions for this component's props type properties
@@ -131,7 +144,12 @@ class CompletionsProviderImpl {
131
144
  completionItems = completionItems.map((item) => {
132
145
  return { ...item, sortText: '_' };
133
146
  });
134
- return completionItems;
147
+ this.lastCompletion = {
148
+ tag: componentName,
149
+ documentVersion: componentSnapshot.version,
150
+ completions: completionItems,
151
+ };
152
+ return { completions: completionItems, componentFilePath: filePath };
135
153
  }
136
154
  getImportedSymbol(sourceFile, identifier) {
137
155
  for (let list of sourceFile.getChildren()) {
@@ -159,24 +177,20 @@ class CompletionsProviderImpl {
159
177
  }
160
178
  return null;
161
179
  }
162
- getPropType(type, typeChecker) {
163
- const sym = type?.getSymbol();
164
- if (!sym) {
165
- return null;
166
- }
167
- for (const decl of sym?.getDeclarations() || []) {
180
+ getPropType(declarations, typeChecker) {
181
+ for (const decl of declarations) {
168
182
  const fileName = (0, utils_3.toVirtualFilePath)(decl.getSourceFile().fileName);
169
- if (fileName.endsWith('.tsx') || fileName.endsWith('.jsx')) {
170
- if (!typescript_1.default.isFunctionDeclaration(decl)) {
171
- console.error(`We only support function components for tsx/jsx at the moment.`);
183
+ if (fileName.endsWith('.tsx') || fileName.endsWith('.jsx') || fileName.endsWith('.d.ts')) {
184
+ if (!typescript_1.default.isFunctionDeclaration(decl) && !typescript_1.default.isFunctionTypeNode(decl)) {
185
+ console.error(`We only support functions declarations at the moment`);
172
186
  continue;
173
187
  }
174
188
  const fn = decl;
175
189
  if (!fn.parameters.length)
176
190
  continue;
177
191
  const param1 = fn.parameters[0];
178
- const type = typeChecker.getTypeAtLocation(param1);
179
- return type;
192
+ const propType = typeChecker.getTypeAtLocation(param1);
193
+ return propType;
180
194
  }
181
195
  }
182
196
  return null;
@@ -216,28 +230,5 @@ class CompletionsProviderImpl {
216
230
  }
217
231
  return item;
218
232
  }
219
- async isAstroComponent(document, node) {
220
- const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
221
- // Get the source file
222
- const tsFilePath = (0, utils_3.toVirtualAstroFilePath)(tsDoc.filePath);
223
- const program = lang.getProgram();
224
- const sourceFile = program?.getSourceFile(tsFilePath);
225
- const typeChecker = program?.getTypeChecker();
226
- if (!sourceFile || !typeChecker) {
227
- return false;
228
- }
229
- const componentName = node.tag;
230
- const imp = this.getImportedSymbol(sourceFile, componentName);
231
- const importType = imp && typeChecker.getTypeAtLocation(imp);
232
- if (!importType) {
233
- return false;
234
- }
235
- const symbolDeclaration = importType.getSymbol()?.declarations;
236
- if (symbolDeclaration) {
237
- const fileName = symbolDeclaration[0].getSourceFile().fileName;
238
- return fileName.endsWith('.astro');
239
- }
240
- return false;
241
- }
242
233
  }
243
234
  exports.CompletionsProviderImpl = CompletionsProviderImpl;
@@ -23,8 +23,6 @@ class CodeActionsProviderImpl {
23
23
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
24
24
  const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
25
25
  const fragment = await tsDoc.createFragment();
26
- const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
27
- const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
28
26
  const tsPreferences = await this.configManager.getTSPreferences(document);
29
27
  const formatOptions = await this.configManager.getTSFormatConfig(document);
30
28
  let result = [];
@@ -50,24 +48,57 @@ class CodeActionsProviderImpl {
50
48
  .map((diag) => Number(diag.code))
51
49
  // We currently cannot support quick fix for unreachable code properly due to the way our TSX output is structured
52
50
  .filter((code) => code !== 7027);
53
- let codeFixes = errorCodes.includes(2304)
54
- ? this.getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences)
55
- : undefined;
56
- codeFixes =
57
- codeFixes ?? lang.getCodeFixesAtPosition(filePath, start, end, errorCodes, formatOptions, tsPreferences);
58
- const codeActions = codeFixes.map((fix) => codeFixToCodeAction(fix, context.diagnostics, context.only ? vscode_languageserver_types_1.CodeActionKind.QuickFix : vscode_languageserver_types_1.CodeActionKind.Empty));
51
+ const html = document.html;
52
+ const node = html.findNodeAt(document.offsetAt(range.start));
53
+ let codeFixes;
54
+ let isInsideScript = false;
55
+ if (node.tag === 'script') {
56
+ const { snapshot: scriptTagSnapshot, filePath: scriptFilePath } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node);
57
+ const start = scriptTagSnapshot.offsetAt(scriptTagSnapshot.getGeneratedPosition(range.start));
58
+ const end = scriptTagSnapshot.offsetAt(scriptTagSnapshot.getGeneratedPosition(range.end));
59
+ codeFixes = lang.getCodeFixesAtPosition(scriptFilePath, start, end, errorCodes, formatOptions, tsPreferences);
60
+ codeFixes = codeFixes.map((fix) => ({
61
+ ...fix,
62
+ changes: mapScriptTagFixToOriginal(fix.changes, scriptTagSnapshot),
63
+ }));
64
+ isInsideScript = true;
65
+ }
66
+ else {
67
+ const start = fragment.offsetAt(fragment.getGeneratedPosition(range.start));
68
+ const end = fragment.offsetAt(fragment.getGeneratedPosition(range.end));
69
+ codeFixes = errorCodes.includes(2304)
70
+ ? this.getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences)
71
+ : undefined;
72
+ codeFixes =
73
+ codeFixes ?? lang.getCodeFixesAtPosition(filePath, start, end, errorCodes, formatOptions, tsPreferences);
74
+ }
75
+ const codeActions = codeFixes.map((fix) => codeFixToCodeAction(fix, context.diagnostics, context.only ? vscode_languageserver_types_1.CodeActionKind.QuickFix : vscode_languageserver_types_1.CodeActionKind.Empty, isInsideScript));
59
76
  result.push(...codeActions);
60
77
  }
61
78
  return result;
62
- function codeFixToCodeAction(codeFix, diagnostics, kind) {
79
+ function codeFixToCodeAction(codeFix, diagnostics, kind, isInsideScript) {
63
80
  const documentChanges = codeFix.changes.map((change) => {
64
81
  return vscode_languageserver_types_1.TextDocumentEdit.create(vscode_languageserver_types_1.OptionalVersionedTextDocumentIdentifier.create(document.getURL(), null), change.textChanges.map((edit) => {
65
82
  let originalRange = (0, documents_1.mapRangeToOriginal)(fragment, (0, utils_2.convertRange)(fragment, edit.span));
66
- if (codeFix.fixName === 'import') {
67
- return (0, CompletionsProvider_1.codeActionChangeToTextEdit)(document, fragment, edit);
83
+ // Inside scripts, we don't need to restrain the insertion of code inside a specific zone as it will be
84
+ // restricted to the area of the script tag by default
85
+ if (!isInsideScript) {
86
+ if (codeFix.fixName === 'import') {
87
+ return (0, CompletionsProvider_1.codeActionChangeToTextEdit)(document, fragment, false, edit);
88
+ }
89
+ if (codeFix.fixName === 'fixMissingFunctionDeclaration') {
90
+ originalRange = (0, utils_2.checkEndOfFileCodeInsert)(originalRange, document);
91
+ }
68
92
  }
69
- if (codeFix.fixName === 'fixMissingFunctionDeclaration') {
70
- originalRange = (0, utils_2.checkEndOfFileCodeInsert)(originalRange, document);
93
+ else {
94
+ // Make sure new imports are not added on the file line of the script tag
95
+ if (codeFix.fixName === 'import') {
96
+ const existingLine = (0, documents_1.getLineAtPosition)(document.positionAt(edit.span.start), document.getText());
97
+ const isNewImport = !existingLine.trim().startsWith('import');
98
+ if (!(edit.newText.startsWith('\n') || edit.newText.startsWith('\r\n')) && isNewImport) {
99
+ edit.newText = typescript_1.default.sys.newLine + edit.newText;
100
+ }
101
+ }
71
102
  }
72
103
  return vscode_languageserver_types_1.TextEdit.replace(originalRange, edit.newText);
73
104
  }));
@@ -78,6 +109,15 @@ class CodeActionsProviderImpl {
78
109
  codeAction.diagnostics = diagnostics;
79
110
  return codeAction;
80
111
  }
112
+ function mapScriptTagFixToOriginal(changes, scriptTagSnapshot) {
113
+ return changes.map((change) => {
114
+ change.textChanges.map((edit) => {
115
+ edit.span.start = fragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(edit.span.start)));
116
+ return edit;
117
+ });
118
+ return change;
119
+ });
120
+ }
81
121
  }
82
122
  getComponentQuickFix(start, end, lang, filePath, formatOptions, tsPreferences) {
83
123
  const sourceFile = lang.getProgram()?.getSourceFile(filePath);
@@ -107,16 +147,35 @@ class CodeActionsProviderImpl {
107
147
  return (0, lodash_1.flatten)(completion.entries.filter((c) => c.name === name || c.name === suffixedName).map(toFix));
108
148
  }
109
149
  async organizeSortImports(document, skipDestructiveCodeActions = false, cancellationToken) {
110
- if (document.astroMeta.frontmatter.state !== 'closed') {
111
- return [];
112
- }
113
150
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
114
151
  const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
115
152
  const fragment = await tsDoc.createFragment();
116
153
  if (cancellationToken?.isCancellationRequested) {
117
154
  return [];
118
155
  }
119
- const changes = lang.organizeImports({ fileName: filePath, type: 'file', skipDestructiveCodeActions }, {}, {});
156
+ let changes = [];
157
+ if (document.astroMeta.frontmatter.state === 'closed') {
158
+ changes.push(...lang.organizeImports({ fileName: filePath, type: 'file', skipDestructiveCodeActions }, {}, {}));
159
+ }
160
+ document.scriptTags.forEach((scriptTag) => {
161
+ const { filePath: scriptFilePath, snapshot: scriptTagSnapshot } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
162
+ const edits = lang.organizeImports({ fileName: scriptFilePath, type: 'file', skipDestructiveCodeActions }, {}, {});
163
+ edits.forEach((edit) => {
164
+ edit.fileName = tsDoc.filePath;
165
+ edit.textChanges = edit.textChanges
166
+ .map((change) => {
167
+ change.span.start = fragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(change.span.start)));
168
+ return change;
169
+ })
170
+ // Since our last line is a (virtual) export, organize imports will try to rewrite it, so let's only take
171
+ // changes that actually happens inside the script tag
172
+ .filter((change) => {
173
+ return scriptTagSnapshot.isInGenerated(document.positionAt(change.span.start));
174
+ });
175
+ return edit;
176
+ });
177
+ changes.push(...edits);
178
+ });
120
179
  const documentChanges = changes.map((change) => {
121
180
  return vscode_languageserver_types_1.TextDocumentEdit.create(vscode_languageserver_types_1.OptionalVersionedTextDocumentIdentifier.create(document.url, null), change.textChanges.map((edit) => {
122
181
  const range = (0, documents_1.mapRangeToOriginal)(fragment, (0, utils_2.convertRange)(fragment, edit.span));
@@ -8,6 +8,7 @@ import { ConfigManager } from '../../../core/config';
8
8
  export interface CompletionItemData extends TextDocumentIdentifier {
9
9
  filePath: string;
10
10
  offset: number;
11
+ scriptTagIndex: number | undefined;
11
12
  originalItem: ts.CompletionEntry;
12
13
  }
13
14
  export declare class CompletionsProviderImpl implements CompletionsProvider<CompletionItemData> {
@@ -32,4 +33,4 @@ export declare class CompletionsProviderImpl implements CompletionsProvider<Comp
32
33
  private getExistingImports;
33
34
  private isAstroComponentImport;
34
35
  }
35
- export declare function codeActionChangeToTextEdit(document: AstroDocument, fragment: AstroSnapshotFragment, change: ts.TextChange): TextEdit;
36
+ export declare function codeActionChangeToTextEdit(document: AstroDocument, fragment: AstroSnapshotFragment, isInsideScriptTag: boolean, change: ts.TextChange): TextEdit;
@@ -65,37 +65,64 @@ class CompletionsProviderImpl {
65
65
  const html = document.html;
66
66
  const offset = document.offsetAt(position);
67
67
  const node = html.findNodeAt(offset);
68
- // TODO: Add support for script tags
69
- if (node.tag === 'script') {
70
- return null;
71
- }
68
+ const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
69
+ let filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
70
+ let completions;
72
71
  const isCompletionInsideFrontmatter = (0, utils_1.isInsideFrontmatter)(document.getText(), offset);
73
72
  const isCompletionInsideExpression = (0, utils_1.isInsideExpression)(document.getText(), node.start, offset);
74
- // PERF: Getting TS completions is fairly slow and I am currently not sure how to speed it up
75
- // As such, we'll try to avoid getting them when unneeded, such as when we're doing HTML stuff
76
- // When at the root of the document TypeScript offer all kinds of completions, because it doesn't know yet that
77
- // it's JSX and not JS. As such, people who are using Emmet to write their template suffer from a very degraded experience
78
- // from what they're used to in HTML files (which is instant completions). So let's disable ourselves when we're at the root
79
- if (!isCompletionInsideFrontmatter && !node.parent && !isCompletionInsideExpression) {
80
- return null;
81
- }
82
- // If the user just typed `<` with nothing else, let's disable ourselves until we're more sure if the user wants TS completions
83
- if (!isCompletionInsideFrontmatter && node.parent && node.tag === undefined && !isCompletionInsideExpression) {
84
- return null;
85
- }
86
- // If the current node is not a component (aka, it doesn't start with a caps), let's disable ourselves as the user
87
- // is most likely looking for HTML completions
88
- if (!isCompletionInsideFrontmatter && !(0, utils_1.isComponentTag)(node) && !isCompletionInsideExpression) {
89
- return null;
90
- }
91
73
  const tsPreferences = await this.configManager.getTSPreferences(document);
92
74
  const formatOptions = await this.configManager.getTSFormatConfig(document);
93
- const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
94
- const filePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
95
- const completions = lang.getCompletionsAtPosition(filePath, offset, {
96
- ...tsPreferences,
97
- triggerCharacter: validTriggerCharacter,
98
- }, formatOptions);
75
+ let scriptTagIndex = undefined;
76
+ if (node.tag === 'script') {
77
+ const { filePath: scriptFilePath, offset: scriptOffset, index: scriptIndex, } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node, position);
78
+ filePath = scriptFilePath;
79
+ scriptTagIndex = scriptIndex;
80
+ completions = lang.getCompletionsAtPosition(scriptFilePath, scriptOffset, {
81
+ ...tsPreferences,
82
+ // File extensions are required inside script tags, however TypeScript can't return completions with the `ts`
83
+ // extension, so what we'll do instead is force `minimal` (aka, no extension) and manually add the extensions
84
+ importModuleSpecifierEnding: 'minimal',
85
+ triggerCharacter: validTriggerCharacter,
86
+ }, formatOptions);
87
+ if (completions) {
88
+ // Manually adds file extensions to js and ts files
89
+ completions.entries = completions?.entries.map((comp) => {
90
+ if (comp.kind === typescript_1.ScriptElementKind.scriptElement &&
91
+ (comp.kindModifiers === '.js' || comp.kindModifiers === '.ts')) {
92
+ return {
93
+ ...comp,
94
+ name: comp.name + comp.kindModifiers,
95
+ };
96
+ }
97
+ else {
98
+ return comp;
99
+ }
100
+ });
101
+ }
102
+ }
103
+ else {
104
+ // PERF: Getting TS completions is fairly slow and I am currently not sure how to speed it up
105
+ // As such, we'll try to avoid getting them when unneeded, such as when we're doing HTML stuff
106
+ // When at the root of the document TypeScript offer all kinds of completions, because it doesn't know yet that
107
+ // it's JSX and not JS. As such, people who are using Emmet to write their template suffer from a very degraded experience
108
+ // from what they're used to in HTML files (which is instant completions). So let's disable ourselves when we're at the root
109
+ if (!isCompletionInsideFrontmatter && !node.parent && !isCompletionInsideExpression) {
110
+ return null;
111
+ }
112
+ // If the user just typed `<` with nothing else, let's disable ourselves until we're more sure if the user wants TS completions
113
+ if (!isCompletionInsideFrontmatter && node.parent && node.tag === undefined && !isCompletionInsideExpression) {
114
+ return null;
115
+ }
116
+ // If the current node is not a component (aka, it doesn't start with a caps), let's disable ourselves as the user
117
+ // is most likely looking for HTML completions
118
+ if (!isCompletionInsideFrontmatter && !(0, utils_1.isComponentTag)(node) && !isCompletionInsideExpression) {
119
+ return null;
120
+ }
121
+ completions = lang.getCompletionsAtPosition(filePath, offset, {
122
+ ...tsPreferences,
123
+ triggerCharacter: validTriggerCharacter,
124
+ }, formatOptions);
125
+ }
99
126
  if (completions === undefined || completions.entries.length === 0) {
100
127
  return null;
101
128
  }
@@ -107,7 +134,7 @@ class CompletionsProviderImpl {
107
134
  const existingImports = this.getExistingImports(document);
108
135
  const completionItems = completions.entries
109
136
  .filter(this.isValidCompletion)
110
- .map((entry) => this.toCompletionItem(fragment, entry, filePath, offset, isCompletionInsideFrontmatter, existingImports))
137
+ .map((entry) => this.toCompletionItem(fragment, entry, filePath, offset, isCompletionInsideFrontmatter, scriptTagIndex, existingImports))
111
138
  .filter(utils_3.isNotNullOrUndefined)
112
139
  .map((comp) => this.fixTextEditRange(wordRangeStartPosition, comp));
113
140
  const completionList = vscode_languageserver_2.CompletionList.create(completionItems, true);
@@ -136,18 +163,29 @@ class CompletionsProviderImpl {
136
163
  item.documentation = itemDocumentation;
137
164
  }
138
165
  const actions = detail?.codeActions;
166
+ const isInsideScriptTag = data.scriptTagIndex !== undefined;
167
+ let scriptTagSnapshot;
168
+ if (isInsideScriptTag) {
169
+ const { snapshot } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, document.scriptTags[data.scriptTagIndex].container);
170
+ scriptTagSnapshot = snapshot;
171
+ }
139
172
  if (actions) {
140
173
  const edit = [];
141
174
  for (const action of actions) {
142
175
  for (const change of action.changes) {
143
- edit.push(...change.textChanges.map((textChange) => codeActionChangeToTextEdit(document, fragment, textChange)));
176
+ if (isInsideScriptTag) {
177
+ change.textChanges.forEach((textChange) => {
178
+ textChange.span.start = fragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(textChange.span.start)));
179
+ });
180
+ }
181
+ edit.push(...change.textChanges.map((textChange) => codeActionChangeToTextEdit(document, fragment, isInsideScriptTag, textChange)));
144
182
  }
145
183
  }
146
184
  item.additionalTextEdits = (item.additionalTextEdits ?? []).concat(edit);
147
185
  }
148
186
  return item;
149
187
  }
150
- toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, existingImports) {
188
+ toCompletionItem(fragment, comp, filePath, offset, insideFrontmatter, scriptTagIndex, existingImports) {
151
189
  let item = vscode_languageserver_protocol_1.CompletionItem.create(comp.name);
152
190
  const isAstroComponent = this.isAstroComponentImport(comp.name);
153
191
  const isImport = comp.insertText?.includes('import');
@@ -194,6 +232,7 @@ class CompletionsProviderImpl {
194
232
  data: {
195
233
  uri: fragment.getURL(),
196
234
  filePath,
235
+ scriptTagIndex,
197
236
  offset,
198
237
  originalItem: comp,
199
238
  },
@@ -273,23 +312,33 @@ class CompletionsProviderImpl {
273
312
  }
274
313
  }
275
314
  exports.CompletionsProviderImpl = CompletionsProviderImpl;
276
- function codeActionChangeToTextEdit(document, fragment, change) {
315
+ function codeActionChangeToTextEdit(document, fragment, isInsideScriptTag, change) {
277
316
  change.newText = (0, utils_2.removeAstroComponentSuffix)(change.newText);
278
- // If we don't have a frontmatter already, create one with the import
279
- const frontmatterState = document.astroMeta.frontmatter.state;
280
- if (frontmatterState === null) {
281
- return vscode_languageserver_1.TextEdit.replace(vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0)), `---${typescript_1.default.sys.newLine}${change.newText}---${typescript_1.default.sys.newLine}${typescript_1.default.sys.newLine}`);
282
- }
283
317
  const { span } = change;
284
318
  let range;
285
319
  const virtualRange = (0, utils_2.convertRange)(fragment, span);
286
320
  range = (0, documents_1.mapRangeToOriginal)(fragment, virtualRange);
287
- if (!(0, utils_1.isInsideFrontmatter)(document.getText(), document.offsetAt(range.start))) {
288
- range = (0, utils_2.ensureFrontmatterInsert)(range, document);
321
+ if (!isInsideScriptTag) {
322
+ // If we don't have a frontmatter already, create one with the import
323
+ const frontmatterState = document.astroMeta.frontmatter.state;
324
+ if (frontmatterState === null) {
325
+ return vscode_languageserver_1.TextEdit.replace(vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0)), `---${typescript_1.default.sys.newLine}${change.newText}---${typescript_1.default.sys.newLine}${typescript_1.default.sys.newLine}`);
326
+ }
327
+ if (!(0, utils_1.isInsideFrontmatter)(document.getText(), document.offsetAt(range.start))) {
328
+ range = (0, utils_2.ensureFrontmatterInsert)(range, document);
329
+ }
330
+ // First import in a file will wrongly have a newline before it due to how the frontmatter is replaced by a comment
331
+ if (range.start.line === 1 && (change.newText.startsWith('\n') || change.newText.startsWith('\r\n'))) {
332
+ change.newText = change.newText.trimStart();
333
+ }
289
334
  }
290
- // First import in a file will wrongly have a newline before it due to how the frontmatter is replaced by a comment
291
- if (range.start.line === 1 && (change.newText.startsWith('\n') || change.newText.startsWith('\r\n'))) {
292
- change.newText = change.newText.trimStart();
335
+ else {
336
+ const existingLine = (0, utils_1.getLineAtPosition)(document.positionAt(span.start), document.getText());
337
+ const isNewImport = !existingLine.trim().startsWith('import');
338
+ // Avoid putting new imports on the same line as the script tag opening
339
+ if (!(change.newText.startsWith('\n') || change.newText.startsWith('\r\n')) && isNewImport) {
340
+ change.newText = typescript_1.default.sys.newLine + change.newText;
341
+ }
293
342
  }
294
343
  return vscode_languageserver_1.TextEdit.replace(range, change.newText);
295
344
  }
@@ -15,7 +15,28 @@ class DefinitionsProviderImpl {
15
15
  const tsFilePath = (0, utils_2.toVirtualAstroFilePath)(tsDoc.filePath);
16
16
  const fragmentPosition = mainFragment.getGeneratedPosition(position);
17
17
  const fragmentOffset = mainFragment.offsetAt(fragmentPosition);
18
- const defs = lang.getDefinitionAndBoundSpan(tsFilePath, fragmentOffset);
18
+ let defs;
19
+ const html = document.html;
20
+ const offset = document.offsetAt(position);
21
+ const node = html.findNodeAt(offset);
22
+ if (node.tag === 'script') {
23
+ const { snapshot: scriptTagSnapshot, filePath: scriptFilePath, offset: scriptOffset, } = (0, utils_2.getScriptTagSnapshot)(tsDoc, document, node, position);
24
+ defs = lang.getDefinitionAndBoundSpan(scriptFilePath, scriptOffset);
25
+ if (defs) {
26
+ defs.definitions = defs.definitions?.map((def) => {
27
+ const isInSameFile = def.fileName === scriptFilePath;
28
+ def.fileName = isInSameFile ? tsFilePath : def.fileName;
29
+ if (isInSameFile) {
30
+ def.textSpan.start = mainFragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(def.textSpan.start)));
31
+ }
32
+ return def;
33
+ });
34
+ defs.textSpan.start = mainFragment.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(defs.textSpan.start)));
35
+ }
36
+ }
37
+ else {
38
+ defs = lang.getDefinitionAndBoundSpan(tsFilePath, fragmentOffset);
39
+ }
19
40
  if (!defs || !defs.definitions) {
20
41
  return [];
21
42
  }
@@ -20,24 +20,52 @@ class DiagnosticsProviderImpl {
20
20
  }
21
21
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
22
22
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
23
+ const fragment = await tsDoc.createFragment();
24
+ let scriptDiagnostics = [];
25
+ document.scriptTags.forEach((scriptTag) => {
26
+ const { filePath: scriptFilePath, snapshot: scriptTagSnapshot } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
27
+ const scriptDiagnostic = [
28
+ ...lang.getSyntacticDiagnostics(scriptFilePath),
29
+ ...lang.getSuggestionDiagnostics(scriptFilePath),
30
+ ...lang.getSemanticDiagnostics(scriptFilePath),
31
+ ]
32
+ // We need to duplicate the diagnostic creation here because we can't map TS's diagnostics range to the original
33
+ // file due to some internal cache inside TS that would cause it to being mapped twice in some cases
34
+ .map((diagnostic) => ({
35
+ range: (0, utils_1.convertRange)(scriptTagSnapshot, diagnostic),
36
+ severity: (0, utils_1.mapSeverity)(diagnostic.category),
37
+ source: 'ts',
38
+ message: typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
39
+ code: diagnostic.code,
40
+ tags: getDiagnosticTag(diagnostic),
41
+ }))
42
+ .map(mapRange(scriptTagSnapshot, document));
43
+ scriptDiagnostics.push(...scriptDiagnostic);
44
+ });
23
45
  const { script: scriptBoundaries } = this.getTagBoundaries(lang, filePath);
24
46
  const syntaxDiagnostics = lang.getSyntacticDiagnostics(filePath);
25
47
  const suggestionDiagnostics = lang.getSuggestionDiagnostics(filePath);
26
- const semanticDiagnostics = lang.getSemanticDiagnostics(filePath).filter((d) => {
27
- return isNoWithinBoundary(scriptBoundaries, d);
48
+ const semanticDiagnostics = lang.getSemanticDiagnostics(filePath);
49
+ const diagnostics = [
50
+ ...syntaxDiagnostics,
51
+ ...suggestionDiagnostics,
52
+ ...semanticDiagnostics,
53
+ ].filter((diag) => {
54
+ return isNoWithinBoundary(scriptBoundaries, diag);
28
55
  });
29
- const diagnostics = [...syntaxDiagnostics, ...suggestionDiagnostics, ...semanticDiagnostics];
30
- const fragment = await tsDoc.createFragment();
31
- return diagnostics
32
- .map((diagnostic) => ({
33
- range: (0, utils_1.convertRange)(tsDoc, diagnostic),
34
- severity: (0, utils_1.mapSeverity)(diagnostic.category),
35
- source: 'ts',
36
- message: typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
37
- code: diagnostic.code,
38
- tags: getDiagnosticTag(diagnostic),
39
- }))
40
- .map(mapRange(fragment, document))
56
+ return [
57
+ ...diagnostics
58
+ .map((diagnostic) => ({
59
+ range: (0, utils_1.convertRange)(tsDoc, diagnostic),
60
+ severity: (0, utils_1.mapSeverity)(diagnostic.category),
61
+ source: 'ts',
62
+ message: typescript_1.default.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
63
+ code: diagnostic.code,
64
+ tags: getDiagnosticTag(diagnostic),
65
+ }))
66
+ .map(mapRange(fragment, document)),
67
+ ...scriptDiagnostics,
68
+ ]
41
69
  .filter((diag) => {
42
70
  return (hasNoNegativeLines(diag) &&
43
71
  isNoJSXImplicitRuntimeWarning(diag) &&
@@ -46,6 +74,7 @@ class DiagnosticsProviderImpl {
46
74
  isNoSpreadExpected(diag) &&
47
75
  isNoCantResolveJSONModule(diag) &&
48
76
  isNoCantReturnOutsideFunction(diag) &&
77
+ isNoIsolatedModuleError(diag) &&
49
78
  isNoJsxCannotHaveMultipleAttrsError(diag));
50
79
  })
51
80
  .map(enhanceIfNecessary);
@@ -146,12 +175,26 @@ function isNoCantReturnOutsideFunction(diagnostic) {
146
175
  function isNoCantResolveJSONModule(diagnostic) {
147
176
  return diagnostic.code !== 2732;
148
177
  }
178
+ /**
179
+ * When the content of the file is invalid and can't be parsed properly for TSX generation, TS will show an error about
180
+ * how the current module can't be compiled under --isolatedModule, this is confusing to users so let's ignore this
181
+ */
182
+ function isNoIsolatedModuleError(diagnostic) {
183
+ return diagnostic.code !== 1208;
184
+ }
149
185
  /**
150
186
  * Some diagnostics have JSX-specific nomenclature or unclear description. Enhance them for more clarity.
151
187
  */
152
188
  function enhanceIfNecessary(diagnostic) {
189
+ // JSX element has no closing tag. JSX -> HTML
190
+ if (diagnostic.code === 17008) {
191
+ return {
192
+ ...diagnostic,
193
+ message: diagnostic.message.replace('JSX', 'HTML'),
194
+ };
195
+ }
196
+ // For the rare case where an user might try to put a client directive on something that is not a component
153
197
  if (diagnostic.code === 2322) {
154
- // For the rare case where an user might try to put a client directive on something that is not a component
155
198
  if (diagnostic.message.includes("Property 'client:") && diagnostic.message.includes("to type 'HTMLProps")) {
156
199
  return {
157
200
  ...diagnostic,
@@ -15,15 +15,22 @@ class FoldingRangesProviderImpl {
15
15
  const html = document.html;
16
16
  const { lang, tsDoc } = await this.languageServiceManager.getLSAndTSDoc(document);
17
17
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
18
- const outliningSpans = lang.getOutliningSpans(filePath);
19
- const foldingRanges = [];
20
- for (const span of outliningSpans) {
18
+ const outliningSpans = lang.getOutliningSpans(filePath).filter((span) => {
21
19
  const node = html.findNodeAt(span.textSpan.start);
22
20
  // Due to how our TSX output transform those tags into function calls or template literals
23
21
  // TypeScript thinks of those as outlining spans, which is fine but we don't want folding ranges for those
24
- if (node.tag === 'script' || node.tag === 'Markdown' || node.tag === 'style') {
25
- continue;
26
- }
22
+ return node.tag !== 'script' && node.tag !== 'style' && node.tag !== 'Markdown';
23
+ });
24
+ const scriptOutliningSpans = [];
25
+ document.scriptTags.forEach((scriptTag) => {
26
+ const { snapshot: scriptTagSnapshot, filePath: scriptFilePath } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, scriptTag.container);
27
+ scriptOutliningSpans.push(...lang.getOutliningSpans(scriptFilePath).map((span) => {
28
+ span.textSpan.start = document.offsetAt(scriptTagSnapshot.getOriginalPosition(scriptTagSnapshot.positionAt(span.textSpan.start)));
29
+ return span;
30
+ }));
31
+ });
32
+ const foldingRanges = [];
33
+ for (const span of [...outliningSpans, ...scriptOutliningSpans]) {
27
34
  const start = document.positionAt(span.textSpan.start);
28
35
  const end = adjustFoldingEnd(start, document.positionAt(span.textSpan.start + span.textSpan.length), document);
29
36
  // When using this method for generating folding ranges, TypeScript tend to return some
@@ -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
  }
@@ -20,8 +20,16 @@ class SignatureHelpProviderImpl {
20
20
  }
21
21
  const filePath = (0, utils_1.toVirtualAstroFilePath)(tsDoc.filePath);
22
22
  const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
23
+ const node = document.html.findNodeAt(offset);
24
+ let info;
23
25
  const triggerReason = this.toTsTriggerReason(context);
24
- const info = lang.getSignatureHelpItems(filePath, offset, triggerReason ? { triggerReason } : undefined);
26
+ if (node.tag === 'script') {
27
+ const { filePath: scriptFilePath, offset: scriptOffset } = (0, utils_1.getScriptTagSnapshot)(tsDoc, document, node, position);
28
+ info = lang.getSignatureHelpItems(scriptFilePath, scriptOffset, triggerReason ? { triggerReason } : undefined);
29
+ }
30
+ else {
31
+ info = lang.getSignatureHelpItems(filePath, offset, triggerReason ? { triggerReason } : undefined);
32
+ }
25
33
  if (!info) {
26
34
  return null;
27
35
  }
@@ -33,6 +33,7 @@ const utils_1 = require("../../utils");
33
33
  const module_loader_1 = require("./module-loader");
34
34
  const SnapshotManager_1 = require("./snapshots/SnapshotManager");
35
35
  const utils_2 = require("./utils");
36
+ const DocumentSnapshot_1 = require("./snapshots/DocumentSnapshot");
36
37
  const DocumentSnapshotUtils = __importStar(require("./snapshots/utils"));
37
38
  const services = new Map();
38
39
  async function getLanguageService(path, workspaceUris, docContext) {
@@ -140,6 +141,12 @@ async function createLanguageService(tsconfigPath, docContext, workspaceUris) {
140
141
  }
141
142
  const newSnapshot = DocumentSnapshotUtils.createFromDocument(document);
142
143
  snapshotManager.set(filePath, newSnapshot);
144
+ document.scriptTags.forEach((scriptTag, index) => {
145
+ const scriptFilePath = filePath + `.__script${index}.js`;
146
+ const scriptSnapshot = new DocumentSnapshot_1.ScriptTagDocumentSnapshot(scriptTag, document, scriptFilePath);
147
+ snapshotManager.set(scriptFilePath, scriptSnapshot);
148
+ newSnapshot.scriptTagSnapshots?.push(scriptSnapshot);
149
+ });
143
150
  if (prevSnapshot && prevSnapshot.scriptKind !== newSnapshot.scriptKind) {
144
151
  // Restart language service as it doesn't handle script kind changes.
145
152
  languageService.dispose();
@@ -166,6 +173,16 @@ async function createLanguageService(tsconfigPath, docContext, workspaceUris) {
166
173
  astroModuleLoader.deleteUnresolvedResolutionsFromCache(fileName);
167
174
  doc = DocumentSnapshotUtils.createFromFilePath(fileName, docContext.createDocument);
168
175
  snapshotManager.set(fileName, doc);
176
+ // If we needed to create an Astro snapshot, also create its script tags snapshots
177
+ if ((0, utils_2.isAstroFilePath)(fileName)) {
178
+ const document = doc.parent;
179
+ document.scriptTags.forEach((scriptTag, index) => {
180
+ const scriptFilePath = fileName + `.__script${index}.js`;
181
+ const scriptSnapshot = new DocumentSnapshot_1.ScriptTagDocumentSnapshot(scriptTag, document, scriptFilePath);
182
+ snapshotManager.set(scriptFilePath, scriptSnapshot);
183
+ doc.scriptTagSnapshots?.push(scriptSnapshot);
184
+ });
185
+ }
169
186
  return doc;
170
187
  }
171
188
  function updateProjectFiles() {
@@ -214,6 +231,7 @@ async function createLanguageService(tsconfigPath, docContext, workspaceUris) {
214
231
  jsxFactory: 'astroHTML',
215
232
  module: typescript_1.default.ModuleKind.ESNext,
216
233
  target: typescript_1.default.ScriptTarget.ESNext,
234
+ isolatedModules: true,
217
235
  moduleResolution: typescript_1.default.ModuleResolutionKind.NodeJs,
218
236
  };
219
237
  const project = typescript_1.default.parseJsonConfigFileContent(configJson, typescript_1.default.sys, tsconfigRoot, forcedCompilerOptions, tsconfigPath, undefined, [
@@ -1,6 +1,6 @@
1
1
  import ts from 'typescript';
2
2
  import { Position, TextDocumentContentChangeEvent } from 'vscode-languageserver';
3
- import { AstroDocument, DocumentMapper, IdentityMapper } from '../../../core/documents';
3
+ import { AstroDocument, DocumentMapper, IdentityMapper, FragmentMapper, TagInformation } from '../../../core/documents';
4
4
  import { FrameworkExt } from '../utils';
5
5
  export interface DocumentSnapshot extends ts.IScriptSnapshot {
6
6
  version: number;
@@ -36,11 +36,12 @@ export interface SnapshotFragment extends DocumentMapper {
36
36
  * Snapshots used for Astro files
37
37
  */
38
38
  export declare class AstroSnapshot implements DocumentSnapshot {
39
- private readonly parent;
39
+ readonly parent: AstroDocument;
40
40
  private readonly text;
41
41
  readonly scriptKind: ts.ScriptKind;
42
42
  private fragment?;
43
43
  version: number;
44
+ scriptTagSnapshots: ScriptTagDocumentSnapshot[];
44
45
  constructor(parent: AstroDocument, text: string, scriptKind: ts.ScriptKind);
45
46
  createFragment(): Promise<AstroSnapshotFragment>;
46
47
  destroyFragment(): null;
@@ -65,6 +66,25 @@ export declare class AstroSnapshotFragment implements SnapshotFragment {
65
66
  isInGenerated(pos: Position): boolean;
66
67
  getURL(): string;
67
68
  }
69
+ export declare class ScriptTagDocumentSnapshot extends FragmentMapper implements DocumentSnapshot, SnapshotFragment {
70
+ scriptTag: TagInformation;
71
+ private readonly parent;
72
+ filePath: string;
73
+ readonly version: number;
74
+ private text;
75
+ scriptKind: ts.ScriptKind;
76
+ private lineOffsets?;
77
+ constructor(scriptTag: TagInformation, parent: AstroDocument, filePath: string);
78
+ positionAt(offset: number): Position;
79
+ offsetAt(position: Position): number;
80
+ createFragment(): Promise<SnapshotFragment>;
81
+ destroyFragment(): void;
82
+ getText(start: number, end: number): string;
83
+ getLength(): number;
84
+ getFullText(): string;
85
+ getChangeRange(): undefined;
86
+ private getLineOffsets;
87
+ }
68
88
  /**
69
89
  * Snapshot used for anything that is not an Astro file
70
90
  * It's both used for .js(x)/.ts(x) files and .svelte/.vue files
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TypeScriptDocumentSnapshot = exports.AstroSnapshotFragment = exports.AstroSnapshot = void 0;
6
+ exports.TypeScriptDocumentSnapshot = exports.ScriptTagDocumentSnapshot = exports.AstroSnapshotFragment = exports.AstroSnapshot = void 0;
7
+ const typescript_1 = __importDefault(require("typescript"));
4
8
  const documents_1 = require("../../../core/documents");
5
9
  const utils_1 = require("../../../utils");
6
10
  const utils_2 = require("../utils");
@@ -13,6 +17,7 @@ class AstroSnapshot {
13
17
  this.text = text;
14
18
  this.scriptKind = scriptKind;
15
19
  this.version = this.parent.version;
20
+ this.scriptTagSnapshots = [];
16
21
  }
17
22
  async createFragment() {
18
23
  if (!this.fragment) {
@@ -72,6 +77,48 @@ class AstroSnapshotFragment {
72
77
  }
73
78
  }
74
79
  exports.AstroSnapshotFragment = AstroSnapshotFragment;
80
+ class ScriptTagDocumentSnapshot extends documents_1.FragmentMapper {
81
+ constructor(scriptTag, parent, filePath) {
82
+ super(parent.getText(), scriptTag, filePath);
83
+ this.scriptTag = scriptTag;
84
+ this.parent = parent;
85
+ this.filePath = filePath;
86
+ this.version = this.parent.version;
87
+ this.text = this.parent.getText().slice(this.scriptTag.start, this.scriptTag.end) + '\nexport {}';
88
+ this.scriptKind = typescript_1.default.ScriptKind.JS;
89
+ }
90
+ positionAt(offset) {
91
+ return (0, documents_1.positionAt)(offset, this.text, this.getLineOffsets());
92
+ }
93
+ offsetAt(position) {
94
+ return (0, documents_1.offsetAt)(position, this.text, this.getLineOffsets());
95
+ }
96
+ async createFragment() {
97
+ return this;
98
+ }
99
+ destroyFragment() {
100
+ //
101
+ }
102
+ getText(start, end) {
103
+ return this.text.substring(start, end);
104
+ }
105
+ getLength() {
106
+ return this.text.length;
107
+ }
108
+ getFullText() {
109
+ return this.text;
110
+ }
111
+ getChangeRange() {
112
+ return undefined;
113
+ }
114
+ getLineOffsets() {
115
+ if (!this.lineOffsets) {
116
+ this.lineOffsets = (0, documents_1.getLineOffsets)(this.text);
117
+ }
118
+ return this.lineOffsets;
119
+ }
120
+ }
121
+ exports.ScriptTagDocumentSnapshot = ScriptTagDocumentSnapshot;
75
122
  /**
76
123
  * Snapshot used for anything that is not an Astro file
77
124
  * It's both used for .js(x)/.ts(x) files and .svelte/.vue files
@@ -187,6 +187,7 @@ class SnapshotManager {
187
187
  const projectFiles = this.getProjectFileNames();
188
188
  let allFiles = Array.from(new Set([...projectFiles, ...this.getFileNames()]));
189
189
  allFiles = allFiles.map((file) => (0, utils_2.ensureRealFilePath)(file));
190
+ // eslint-disable-next-line no-console
190
191
  console.log('SnapshotManager File Statistics:\n' +
191
192
  `Project files: ${projectFiles.length}\n` +
192
193
  `Astro files: ${allFiles.filter((name) => name.endsWith('.astro')).length}\n` +
@@ -1,7 +1,8 @@
1
1
  import ts from 'typescript';
2
2
  import { CompletionItemKind, DiagnosticSeverity, Position, Range, SymbolKind, SemanticTokensLegend } from 'vscode-languageserver';
3
3
  import { AstroDocument } from '../../core/documents';
4
- import { SnapshotFragment } from './snapshots/DocumentSnapshot';
4
+ import { AstroSnapshot, ScriptTagDocumentSnapshot, SnapshotFragment } from './snapshots/DocumentSnapshot';
5
+ import { Node } from 'vscode-html-languageservice';
5
6
  export declare const enum TokenType {
6
7
  class = 0,
7
8
  enum = 1,
@@ -58,4 +59,13 @@ export declare function toVirtualFilePath(filePath: string): string;
58
59
  export declare function toRealAstroFilePath(filePath: string): string;
59
60
  export declare function ensureRealAstroFilePath(filePath: string): string;
60
61
  export declare function ensureRealFilePath(filePath: string): string;
62
+ export declare function getScriptTagSnapshot(snapshot: AstroSnapshot, document: AstroDocument, tagInfo: Node | {
63
+ start: number;
64
+ end: number;
65
+ }, position?: Position): {
66
+ snapshot: ScriptTagDocumentSnapshot;
67
+ filePath: string;
68
+ index: number;
69
+ offset: number;
70
+ };
61
71
  export {};
@@ -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.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;
6
+ exports.getScriptTagSnapshot = 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");
@@ -346,3 +346,19 @@ function ensureRealFilePath(filePath) {
346
346
  }
347
347
  }
348
348
  exports.ensureRealFilePath = ensureRealFilePath;
349
+ function getScriptTagSnapshot(snapshot, document, tagInfo, position) {
350
+ const index = document.scriptTags.findIndex((value) => value.container.start == tagInfo.start);
351
+ const scriptFilePath = snapshot.filePath + `.__script${index}.js`;
352
+ const scriptTagSnapshot = snapshot.scriptTagSnapshots[index];
353
+ let offset = 0;
354
+ if (position) {
355
+ offset = scriptTagSnapshot.offsetAt(scriptTagSnapshot.getGeneratedPosition(position));
356
+ }
357
+ return {
358
+ snapshot: scriptTagSnapshot,
359
+ filePath: scriptFilePath,
360
+ index,
361
+ offset,
362
+ };
363
+ }
364
+ exports.getScriptTagSnapshot = getScriptTagSnapshot;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/language-server",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "author": "withastro",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",