summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--tools/language-server/src/index.ts2
-rw-r--r--tools/language-server/src/plugins/astro/AstroPlugin.ts168
-rw-r--r--tools/language-server/src/plugins/html/HTMLPlugin.ts15
-rw-r--r--tools/language-server/src/plugins/typescript/LanguageServiceManager.ts6
4 files changed, 173 insertions, 18 deletions
diff --git a/tools/language-server/src/index.ts b/tools/language-server/src/index.ts
index 5f741d1f7..e029684cb 100644
--- a/tools/language-server/src/index.ts
+++ b/tools/language-server/src/index.ts
@@ -23,10 +23,10 @@ export function startServer() {
filterIncompleteCompletions: !evt.initializationOptions?.dontFilterIncompleteCompletions,
definitionLinkSupport: !!evt.capabilities.textDocument?.definition?.linkSupport,
});
- pluginHost.register(new AstroPlugin(docManager, configManager, workspaceUris));
pluginHost.register(new HTMLPlugin(docManager, configManager));
pluginHost.register(new CSSPlugin(docManager, configManager));
pluginHost.register(new TypeScriptPlugin(docManager, configManager, workspaceUris));
+ pluginHost.register(new AstroPlugin(docManager, configManager, workspaceUris));
configManager.updateEmmetConfig(evt.initializationOptions?.configuration?.emmet || evt.initializationOptions?.emmetConfig || {});
return {
diff --git a/tools/language-server/src/plugins/astro/AstroPlugin.ts b/tools/language-server/src/plugins/astro/AstroPlugin.ts
index d8c08089d..dcd0f8cec 100644
--- a/tools/language-server/src/plugins/astro/AstroPlugin.ts
+++ b/tools/language-server/src/plugins/astro/AstroPlugin.ts
@@ -8,14 +8,18 @@ import {
CompletionList,
CompletionItem,
CompletionItemKind,
+ CompletionTriggerKind,
InsertTextFormat,
LocationLink,
FoldingRange,
+ MarkupContent,
+ MarkupKind,
Range,
TextEdit,
} from 'vscode-languageserver';
import { Node } from 'vscode-html-languageservice';
import { isPossibleClientComponent, pathToUrl, urlToPath } from '../../utils';
+import { toVirtualAstroFilePath } from '../typescript/utils';
import { isInsideFrontmatter } from '../../core/documents/utils';
import * as ts from 'typescript';
import { LanguageServiceManager as TypeScriptLanguageServiceManager } from '../typescript/LanguageServiceManager';
@@ -50,6 +54,13 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
if (clientHint) items.push(...clientHint);
}
+ if (!this.isInsideFrontmatter(document, position)) {
+ const props = await this.getPropCompletions(document, position, completionContext);
+ if(props.length) {
+ items.push(...props);
+ }
+ }
+
return CompletionList.create(items, true);
}
@@ -88,27 +99,13 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
const [componentName] = node.tag!.split(':');
- const filePath = urlToPath(document.uri);
- const tsFilePath = filePath + '.ts';
-
- const { lang, tsDoc } = await this.tsLanguageServiceManager.getTypeScriptDoc(document);
+ const { lang } = await this.tsLanguageServiceManager.getTypeScriptDoc(document);
+ const defs = this.getDefinitionsForComponentName(document, lang, componentName);
- const sourceFile = lang.getProgram()?.getSourceFile(tsFilePath);
- if (!sourceFile) {
- return [];
- }
-
- const specifier = this.getImportSpecifierForIdentifier(sourceFile, componentName);
- if (!specifier) {
- return [];
- }
-
- const defs = lang.getDefinitionAtPosition(tsFilePath, specifier.getStart());
if (!defs) {
return [];
}
- const tsFragment = await tsDoc.getFragment();
const startRange: Range = Range.create(Position.create(0, 0), Position.create(0, 0));
const links = defs.map((def) => {
const defFilePath = ensureRealFilePath(def.fileName);
@@ -170,6 +167,101 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
return null;
}
+ private async getPropCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<CompletionItem[]> {
+ const offset = document.offsetAt(position);
+ const html = document.html;
+
+ const node = html.findNodeAt(offset);
+ if(!this.isComponentTag(node)) {
+ return [];
+ }
+ const inAttribute = node.start + node.tag!.length < offset;
+ if(!inAttribute) {
+ return [];
+ }
+
+ // If inside of attributes, skip.
+ if(completionContext && completionContext.triggerKind === CompletionTriggerKind.TriggerCharacter && completionContext.triggerCharacter === '"') {
+ return [];
+ }
+
+ const componentName = node.tag!;
+ const { lang: thisLang } = await this.tsLanguageServiceManager.getTypeScriptDoc(document);
+
+ const defs = this.getDefinitionsForComponentName(document, thisLang, componentName);
+
+ if (!defs) {
+ return [];
+ }
+
+ const defFilePath = ensureRealFilePath(defs[0].fileName);
+
+ const lang = await this.tsLanguageServiceManager.getTypeScriptLangForPath(defFilePath);
+ const program = lang.getProgram();
+ const sourceFile = program?.getSourceFile(toVirtualAstroFilePath(defFilePath));
+ const typeChecker = program?.getTypeChecker();
+
+ if(!sourceFile || !typeChecker) {
+ return [];
+ }
+
+ let propsNode = this.getPropsNode(sourceFile);
+ if(!propsNode) {
+ return [];
+ }
+
+ const completionItems: CompletionItem[] = [];
+
+ for(let type of typeChecker.getBaseTypes(propsNode as unknown as ts.InterfaceType)) {
+ type.symbol.members!.forEach(mem => {
+ let item: CompletionItem = {
+ label: mem.name,
+ insertText: mem.name,
+ commitCharacters: []
+ };
+
+ mem.getDocumentationComment(typeChecker);
+ let description = mem.getDocumentationComment(typeChecker).map(val => val.text).join('\n');
+
+ if(description) {
+ let docs: MarkupContent = {
+ kind: MarkupKind.Markdown,
+ value: description
+ };
+ item.documentation = docs;
+ }
+ completionItems.push(item);
+ });
+ }
+
+ for(let member of propsNode.members) {
+ if(!member.name) continue;
+
+ let name = member.name.getText();
+ let symbol = typeChecker.getSymbolAtLocation(member.name);
+ if(!symbol) continue;
+ let description = symbol.getDocumentationComment(typeChecker).map(val => val.text).join('\n');
+
+ let item: CompletionItem = {
+ label: name,
+ insertText: name,
+ commitCharacters: []
+ };
+
+ if(description) {
+ let docs: MarkupContent = {
+ kind: MarkupKind.Markdown,
+ value: description
+ };
+ item.documentation = docs;
+ }
+
+ completionItems.push(item);
+ }
+
+ return completionItems;
+ }
+
private isInsideFrontmatter(document: Document, position: Position) {
return isInsideFrontmatter(document.getText(), document.offsetAt(position));
}
@@ -182,6 +274,28 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
return /[A-Z]/.test(firstChar);
}
+ private getDefinitionsForComponentName(document: Document, lang: ts.LanguageService, componentName: string): readonly ts.DefinitionInfo[] | undefined {
+ const filePath = urlToPath(document.uri);
+ const tsFilePath = toVirtualAstroFilePath(filePath!);
+
+ const sourceFile = lang.getProgram()?.getSourceFile(tsFilePath);
+ if (!sourceFile) {
+ return undefined;
+ }
+
+ const specifier = this.getImportSpecifierForIdentifier(sourceFile, componentName);
+ if (!specifier) {
+ return [];
+ }
+
+ const defs = lang.getDefinitionAtPosition(tsFilePath, specifier.getStart());
+ if (!defs) {
+ return undefined;
+ }
+
+ return defs;
+ }
+
private getImportSpecifierForIdentifier(sourceFile: ts.SourceFile, identifier: string): ts.Expression | undefined {
let importSpecifier: ts.Expression | undefined = undefined;
ts.forEachChild(sourceFile, (tsNode) => {
@@ -197,4 +311,26 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
});
return importSpecifier;
}
+
+ private getPropsNode(sourceFile: ts.SourceFile): ts.InterfaceDeclaration | null {
+ let found: ts.InterfaceDeclaration | null = null;
+ ts.forEachChild(sourceFile, node => {
+ if(isNodeExported(node)) {
+ if(ts.isInterfaceDeclaration(node)) {
+ if(ts.getNameOfDeclaration(node)?.getText() === 'Props') {
+ found = node;
+ }
+ }
+ }
+ });
+
+ return found;
+ }
}
+
+function isNodeExported(node: ts.Node): boolean {
+ return (
+ (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 ||
+ (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
+ );
+} \ No newline at end of file
diff --git a/tools/language-server/src/plugins/html/HTMLPlugin.ts b/tools/language-server/src/plugins/html/HTMLPlugin.ts
index d4f75e0d3..90c55b502 100644
--- a/tools/language-server/src/plugins/html/HTMLPlugin.ts
+++ b/tools/language-server/src/plugins/html/HTMLPlugin.ts
@@ -31,6 +31,13 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
return null;
}
+ const offset = document.offsetAt(position);
+ const node = html.findNodeAt(offset);
+
+ if(this.isComponentTag(node)) {
+ return null;
+ }
+
const emmetResults: CompletionList = {
isIncomplete: true,
items: [],
@@ -124,4 +131,12 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
private isInsideFrontmatter(document: Document, position: Position) {
return isInsideFrontmatter(document.getText(), document.offsetAt(position));
}
+
+ private isComponentTag(node: Node): boolean {
+ if (!node.tag) {
+ return false;
+ }
+ const firstChar = node.tag[0];
+ return /[A-Z]/.test(firstChar);
+ }
}
diff --git a/tools/language-server/src/plugins/typescript/LanguageServiceManager.ts b/tools/language-server/src/plugins/typescript/LanguageServiceManager.ts
index 3ebcfdd77..9ff71abf7 100644
--- a/tools/language-server/src/plugins/typescript/LanguageServiceManager.ts
+++ b/tools/language-server/src/plugins/typescript/LanguageServiceManager.ts
@@ -2,7 +2,7 @@ import * as ts from 'typescript';
import type { Document, DocumentManager } from '../../core/documents';
import type { ConfigManager } from '../../core/config';
import { urlToPath, pathToUrl, debounceSameArg } from '../../utils';
-import { getLanguageService, getLanguageServiceForDocument, LanguageServiceContainer, LanguageServiceDocumentContext } from './languageService';
+import { getLanguageService, getLanguageServiceForPath, getLanguageServiceForDocument, LanguageServiceContainer, LanguageServiceDocumentContext } from './languageService';
import { SnapshotManager } from './SnapshotManager';
import { DocumentSnapshot } from './DocumentSnapshot';
@@ -71,6 +71,10 @@ export class LanguageServiceManager {
return { tsDoc, lang };
}
+ async getTypeScriptLangForPath(filePath: string): Promise<ts.LanguageService> {
+ return getLanguageServiceForPath(filePath, this.workspaceUris, this.docContext);
+ }
+
async getSnapshotManager(filePath: string): Promise<SnapshotManager> {
return (await this.getTypeScriptLanguageService(filePath)).snapshotManager;
}