summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@matthewphillips.info> 2021-08-10 09:30:02 -0400
committerGravatar GitHub <noreply@github.com> 2021-08-10 09:30:02 -0400
commit2c5380a26631f720bfbbcec78295c71b562e647d (patch)
tree40ead718c2d817265a8d40bb04d099aae507c4a7
parent1339d5e36bdaae1eeea6bf9d99b2bdf4d69d604a (diff)
downloadastro-2c5380a26631f720bfbbcec78295c71b562e647d.tar.gz
astro-2c5380a26631f720bfbbcec78295c71b562e647d.tar.zst
astro-2c5380a26631f720bfbbcec78295c71b562e647d.zip
Add support for Astro.* completion and Hover help (#1068)
* Add support for Astro.* completion and Hover help * Allow providing a generic type to fetchContent
-rw-r--r--tools/language-server/astro.d.ts27
-rw-r--r--tools/language-server/src/index.ts7
-rw-r--r--tools/language-server/src/plugins/PluginHost.ts47
-rw-r--r--tools/language-server/src/plugins/astro/AstroPlugin.ts1
-rw-r--r--tools/language-server/src/plugins/css/CSSPlugin.ts1
-rw-r--r--tools/language-server/src/plugins/html/HTMLPlugin.ts1
-rw-r--r--tools/language-server/src/plugins/interfaces.ts6
-rw-r--r--tools/language-server/src/plugins/typescript/DocumentSnapshot.ts19
-rw-r--r--tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts39
-rw-r--r--tools/language-server/src/plugins/typescript/astro-sys.ts3
-rw-r--r--tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts62
-rw-r--r--tools/language-server/src/plugins/typescript/features/HoverProvider.ts42
-rw-r--r--tools/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts158
-rw-r--r--tools/language-server/src/plugins/typescript/previewer.ts140
-rw-r--r--tools/language-server/src/plugins/typescript/utils.ts3
15 files changed, 495 insertions, 61 deletions
diff --git a/tools/language-server/astro.d.ts b/tools/language-server/astro.d.ts
new file mode 100644
index 000000000..267f6f108
--- /dev/null
+++ b/tools/language-server/astro.d.ts
@@ -0,0 +1,27 @@
+type AstroRenderedHTML = string;
+
+type FetchContentResult<ContentFrontmatter extends Record<string, any> = Record<string, any>> = {
+ astro: {
+ headers: string[];
+ source: string;
+ html: AstroRenderedHTML;
+ };
+ url: URL;
+} & ContentFrontmatter;
+
+interface AstroPageRequest {
+ url: URL;
+ canonicalURL: URL;
+}
+
+interface Astro {
+ isPage: boolean;
+ fetchContent<ContentFrontmatter>(globStr: string): FetchContentResult<ContentFrontmatter>[];
+ props: Record<string, number | string | any>;
+ request: AstroPageRequest;
+ site: URL;
+}
+
+declare const Astro: Astro;
+
+export default function(): string; \ No newline at end of file
diff --git a/tools/language-server/src/index.ts b/tools/language-server/src/index.ts
index 5e4c736a2..1ca7172c8 100644
--- a/tools/language-server/src/index.ts
+++ b/tools/language-server/src/index.ts
@@ -64,6 +64,11 @@ export function startServer() {
':',
],
},
+ hoverProvider: true,
+ signatureHelpProvider: {
+ triggerCharacters: ['(', ',', '<'],
+ retriggerCharacters: [')']
+ }
},
};
});
@@ -107,9 +112,11 @@ export function startServer() {
return pluginHost.resolveCompletion(data, completionItem);
});
+ connection.onHover((evt) => pluginHost.doHover(evt.textDocument, evt.position));
connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position));
connection.onFoldingRanges((evt) => pluginHost.getFoldingRanges(evt.textDocument));
connection.onRequest(TagCloseRequest, (evt: any) => pluginHost.doTagComplete(evt.textDocument, evt.position));
+ connection.onSignatureHelp((evt, cancellationToken) => pluginHost.getSignatureHelp(evt.textDocument, evt.position, evt.context, cancellationToken));
connection.listen();
}
diff --git a/tools/language-server/src/plugins/PluginHost.ts b/tools/language-server/src/plugins/PluginHost.ts
index 3741845c4..f3e50e4d0 100644
--- a/tools/language-server/src/plugins/PluginHost.ts
+++ b/tools/language-server/src/plugins/PluginHost.ts
@@ -1,8 +1,19 @@
-import { CompletionContext, CompletionItem, CompletionList, DefinitionLink, Location, Position, TextDocumentIdentifier } from 'vscode-languageserver';
+import type {
+ CancellationToken,
+ CompletionContext,
+ CompletionItem,
+ DefinitionLink,
+ Location,
+ Position,
+ SignatureHelp,
+ SignatureHelpContext,
+ TextDocumentIdentifier
+} from 'vscode-languageserver';
import type { DocumentManager } from '../core/documents';
import type * as d from './interfaces';
import { flatten } from '../utils';
-import { FoldingRange } from 'vscode-languageserver-types';
+import { CompletionList } from 'vscode-languageserver';
+import { Hover, FoldingRange } from 'vscode-languageserver-types';
enum ExecuteMode {
None,
@@ -60,6 +71,15 @@ export class PluginHost {
return result ?? completionItem;
}
+ async doHover(textDocument: TextDocumentIdentifier, position: Position): Promise<Hover | null> {
+ const document = this.getDocument(textDocument.uri);
+ if (!document) {
+ throw new Error('Cannot call methods on an unopened document');
+ }
+
+ return this.execute<Hover>('doHover', [document, position], ExecuteMode.FirstNonNull);
+ }
+
async doTagComplete(textDocument: TextDocumentIdentifier, position: Position): Promise<string | null> {
const document = this.getDocument(textDocument.uri);
if (!document) {
@@ -95,6 +115,24 @@ export class PluginHost {
}
}
+ async getSignatureHelp(
+ textDocument: TextDocumentIdentifier,
+ position: Position,
+ context: SignatureHelpContext | undefined,
+ cancellationToken: CancellationToken
+ ): Promise<SignatureHelp | null> {
+ const document = this.getDocument(textDocument.uri);
+ if (!document) {
+ throw new Error('Cannot call methods on an unopened document');
+ }
+
+ return await this.execute<any>(
+ 'getSignatureHelp',
+ [document, position, context, cancellationToken],
+ ExecuteMode.FirstNonNull
+ );
+ }
+
onWatchFileChanges(onWatchFileChangesParams: any[]): void {
for (const support of this.plugins) {
support.onWatchFileChanges?.(onWatchFileChangesParams);
@@ -121,7 +159,10 @@ export class PluginHost {
}
return null;
case ExecuteMode.Collect:
- return Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, [])));
+ return Promise.all(plugins.map((plugin) => {
+ let ret = this.tryExecutePlugin(plugin, name, args, []);
+ return ret;
+ }));
case ExecuteMode.None:
await Promise.all(plugins.map((plugin) => this.tryExecutePlugin(plugin, name, args, null)));
return;
diff --git a/tools/language-server/src/plugins/astro/AstroPlugin.ts b/tools/language-server/src/plugins/astro/AstroPlugin.ts
index 535375eeb..d8c08089d 100644
--- a/tools/language-server/src/plugins/astro/AstroPlugin.ts
+++ b/tools/language-server/src/plugins/astro/AstroPlugin.ts
@@ -26,6 +26,7 @@ export class AstroPlugin implements CompletionsProvider, FoldingRangeProvider {
private readonly docManager: DocumentManager;
private readonly configManager: ConfigManager;
private readonly tsLanguageServiceManager: TypeScriptLanguageServiceManager;
+ public pluginName = 'Astro';
constructor(docManager: DocumentManager, configManager: ConfigManager, workspaceUris: string[]) {
this.docManager = docManager;
diff --git a/tools/language-server/src/plugins/css/CSSPlugin.ts b/tools/language-server/src/plugins/css/CSSPlugin.ts
index 26c90ac66..3083edc56 100644
--- a/tools/language-server/src/plugins/css/CSSPlugin.ts
+++ b/tools/language-server/src/plugins/css/CSSPlugin.ts
@@ -16,6 +16,7 @@ export class CSSPlugin implements CompletionsProvider {
private configManager: ConfigManager;
private documents = new WeakMap<Document, CSSDocument>();
private triggerCharacters = new Set(['.', ':', '-', '/']);
+ public pluginName = 'CSS';
constructor(docManager: DocumentManager, configManager: ConfigManager) {
this.docManager = docManager;
diff --git a/tools/language-server/src/plugins/html/HTMLPlugin.ts b/tools/language-server/src/plugins/html/HTMLPlugin.ts
index 7e0ab4861..d4f75e0d3 100644
--- a/tools/language-server/src/plugins/html/HTMLPlugin.ts
+++ b/tools/language-server/src/plugins/html/HTMLPlugin.ts
@@ -11,6 +11,7 @@ export class HTMLPlugin implements CompletionsProvider, FoldingRangeProvider {
private documents = new WeakMap<Document, HTMLDocument>();
private styleScriptTemplate = new Set(['template', 'style', 'script']);
private configManager: ConfigManager;
+ public pluginName = 'HTML';
constructor(docManager: DocumentManager, configManager: ConfigManager) {
docManager.on('documentChange', (document) => {
diff --git a/tools/language-server/src/plugins/interfaces.ts b/tools/language-server/src/plugins/interfaces.ts
index b68100de1..84e4cbda3 100644
--- a/tools/language-server/src/plugins/interfaces.ts
+++ b/tools/language-server/src/plugins/interfaces.ts
@@ -164,4 +164,8 @@ export interface LSPProviderConfig {
definitionLinkSupport: boolean;
}
-export type Plugin = Partial<ProviderBase & DefinitionsProvider & OnWatchFileChanges & SelectionRangeProvider & UpdateTsOrJsFile>;
+interface NamedPlugin {
+ pluginName: string;
+}
+
+export type Plugin = Partial<NamedPlugin & ProviderBase & DefinitionsProvider & OnWatchFileChanges & SelectionRangeProvider & UpdateTsOrJsFile>;
diff --git a/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts b/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts
index 9b8420f65..d7473c7b3 100644
--- a/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts
+++ b/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts
@@ -1,11 +1,12 @@
import * as ts from 'typescript';
+import { readFileSync } from 'fs';
import { TextDocumentContentChangeEvent, Position } from 'vscode-languageserver';
import { Document, DocumentMapper, IdentityMapper } from '../../core/documents';
import { isInTag, positionAt, offsetAt } from '../../core/documents/utils';
import { pathToUrl } from '../../utils';
import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils';
-const FILLER_DEFAULT_EXPORT = `\nexport default function() { return ''; };`;
+const ASTRO_DEFINITION = readFileSync(require.resolve('../../../astro.d.ts'));
/**
* The mapper to get from original snapshot positions to generated and vice versa.
@@ -74,11 +75,9 @@ class AstroDocumentSnapshot implements DocumentSnapshot {
/** @internal */
private transformContent(content: string) {
- return (
- content.replace(/---/g, '///') +
- // TypeScript needs this to know there's a default export.
- FILLER_DEFAULT_EXPORT
- );
+ return content.replace(/---/g, '///') +
+ // Add TypeScript definitions
+ ASTRO_DEFINITION;
}
get filePath() {
@@ -140,11 +139,9 @@ export class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFrag
/** @internal */
private transformContent(content: string) {
- return (
- content.replace(/---/g, '///') +
- // TypeScript needs this to know there's a default export.
- FILLER_DEFAULT_EXPORT
- );
+ return content.replace(/---/g, '///') +
+ // Add TypeScript definitions
+ ASTRO_DEFINITION;
}
getText(start: number, end: number) {
diff --git a/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts
index 1d3441356..e953452d3 100644
--- a/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts
+++ b/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts
@@ -1,16 +1,24 @@
-import { join as pathJoin, dirname as pathDirname } from 'path';
-import { Document, DocumentManager, isInsideFrontmatter } from '../../core/documents';
import type { ConfigManager } from '../../core/config';
import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces';
+import type {
+ CancellationToken,
+ Hover,
+ SignatureHelp,
+ SignatureHelpContext
+} from 'vscode-languageserver';
+import { join as pathJoin, dirname as pathDirname } from 'path';
+import { Document, DocumentManager, isInsideFrontmatter } from '../../core/documents';
import { SourceFile, ImportDeclaration, Node, SyntaxKind } from 'typescript';
import { CompletionContext, DefinitionLink, FileChangeType, Position, LocationLink } from 'vscode-languageserver';
import * as ts from 'typescript';
-import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider';
import { LanguageServiceManager } from './LanguageServiceManager';
import { SnapshotManager } from './SnapshotManager';
import { convertToLocationRange, isVirtualAstroFilePath, isVirtualFilePath, getScriptKindFromFileName } from './utils';
-import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
import { isNotNullOrUndefined, pathToUrl } from '../../utils';
+import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider';
+import { HoverProviderImpl } from './features/HoverProvider';
+import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
+import { SignatureHelpProviderImpl } from './features/SignatureHelpProvider';
type BetterTS = typeof ts & {
getTouchingPropertyName(sourceFile: SourceFile, pos: number): Node;
@@ -20,8 +28,11 @@ export class TypeScriptPlugin implements CompletionsProvider {
private readonly docManager: DocumentManager;
private readonly configManager: ConfigManager;
private readonly languageServiceManager: LanguageServiceManager;
+ public pluginName = 'TypeScript';
private readonly completionProvider: CompletionsProviderImpl;
+ private readonly hoverProvider: HoverProviderImpl;
+ private readonly signatureHelpProvider: SignatureHelpProviderImpl;
constructor(docManager: DocumentManager, configManager: ConfigManager, workspaceUris: string[]) {
this.docManager = docManager;
@@ -29,6 +40,12 @@ export class TypeScriptPlugin implements CompletionsProvider {
this.languageServiceManager = new LanguageServiceManager(docManager, configManager, workspaceUris);
this.completionProvider = new CompletionsProviderImpl(this.languageServiceManager);
+ this.hoverProvider = new HoverProviderImpl(this.languageServiceManager);
+ this.signatureHelpProvider = new SignatureHelpProviderImpl(this.languageServiceManager);
+ }
+
+ async doHover(document: Document, position: Position): Promise<Hover | null> {
+ return this.hoverProvider.doHover(document, position);
}
async getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
@@ -117,6 +134,20 @@ export class TypeScriptPlugin implements CompletionsProvider {
}
}
+ async getSignatureHelp(
+ document: Document,
+ position: Position,
+ context: SignatureHelpContext | undefined,
+ cancellationToken?: CancellationToken
+ ): Promise<SignatureHelp | null> {
+ return this.signatureHelpProvider.getSignatureHelp(
+ document,
+ position,
+ context,
+ cancellationToken
+ );
+ }
+
/**
*
* @internal
diff --git a/tools/language-server/src/plugins/typescript/astro-sys.ts b/tools/language-server/src/plugins/typescript/astro-sys.ts
index 57cd3b497..c8d23254d 100644
--- a/tools/language-server/src/plugins/typescript/astro-sys.ts
+++ b/tools/language-server/src/plugins/typescript/astro-sys.ts
@@ -9,9 +9,6 @@ export function createAstroSys(getSnapshot: (fileName: string) => DocumentSnapsh
const AstroSys: ts.System = {
...ts.sys,
fileExists(path: string) {
- if (isAstroFilePath(path) || isVirtualAstroFilePath(path)) {
- console.log('fileExists', path, ts.sys.fileExists(ensureRealAstroFilePath(path)));
- }
return ts.sys.fileExists(ensureRealAstroFilePath(path));
},
readFile(path: string) {
diff --git a/tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts b/tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts
index d13269c5c..daeed9766 100644
--- a/tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts
+++ b/tools/language-server/src/plugins/typescript/features/CompletionsProvider.ts
@@ -1,10 +1,17 @@
+import type { CompletionContext, CompletionItem, Position, TextDocumentIdentifier, MarkupContent } from 'vscode-languageserver';
+import type { LanguageServiceManager } from '../LanguageServiceManager';
import { isInsideFrontmatter } from '../../../core/documents/utils';
import { Document } from '../../../core/documents';
import * as ts from 'typescript';
-import { CompletionContext, CompletionList, CompletionItem, Position, TextDocumentIdentifier, TextEdit, MarkupKind, MarkupContent } from 'vscode-languageserver';
+import { CompletionList, MarkupKind } from 'vscode-languageserver';
import { AppCompletionItem, AppCompletionList, CompletionsProvider } from '../../interfaces';
-import type { LanguageServiceManager } from '../LanguageServiceManager';
-import { scriptElementKindToCompletionItemKind, getCommitCharactersForScriptElement } from '../utils';
+import { scriptElementKindToCompletionItemKind, getCommitCharactersForScriptElement, toVirtualAstroFilePath } from '../utils';
+
+const completionOptions: ts.GetCompletionsAtPositionOptions = Object.freeze({
+ importModuleSpecifierPreference: 'relative',
+ importModuleSpecifierEnding: 'js',
+ quotePreference: 'single',
+});
export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier {
position: Position;
@@ -13,7 +20,7 @@ export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDo
export class CompletionsProviderImpl implements CompletionsProvider<CompletionEntryWithIdentifer> {
constructor(private lang: LanguageServiceManager) {}
- async getCompletions(document: Document, position: Position, completionContext?: CompletionContext): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
+ async getCompletions(document: Document, position: Position, _completionContext?: CompletionContext): Promise<AppCompletionList<CompletionEntryWithIdentifer> | null> {
// TODO: handle inside expression
if (!isInsideFrontmatter(document.getText(), document.offsetAt(position))) {
return null;
@@ -26,12 +33,9 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
const fragment = await tsDoc.getFragment();
const offset = document.offsetAt(position);
+
const entries =
- lang.getCompletionsAtPosition(fragment.filePath, offset, {
- importModuleSpecifierPreference: 'relative',
- importModuleSpecifierEnding: 'js',
- quotePreference: 'single',
- })?.entries || [];
+ lang.getCompletionsAtPosition(fragment.filePath, offset, completionOptions)?.entries || [];
const completionItems = entries
.map((entry: ts.CompletionEntry) => this.toCompletionItem(fragment, entry, document.uri, position, new Set()))
@@ -44,18 +48,22 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
const { data: comp } = completionItem;
const { tsDoc, lang } = await this.lang.getTypeScriptDoc(document);
- let filePath = tsDoc.filePath;
+ let filePath = toVirtualAstroFilePath(tsDoc.filePath);
if (!comp || !filePath) {
return completionItem;
}
- if (filePath.endsWith('.astro')) {
- filePath = filePath + '.ts';
- }
-
const fragment = await tsDoc.getFragment();
- const detail = lang.getCompletionEntryDetails(filePath, fragment.offsetAt(comp.position), comp.name, {}, comp.source, {}, undefined);
+ const detail = lang.getCompletionEntryDetails(
+ filePath, // fileName
+ fragment.offsetAt(comp.position), // position
+ comp.name, // entryName
+ {}, // formatOptions
+ comp.source, // source
+ {}, // preferences
+ comp.data // data
+ );
if (detail) {
const { detail: itemDetail, documentation: itemDocumentation } = this.getCompletionDocument(detail);
@@ -64,30 +72,6 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
completionItem.documentation = itemDocumentation;
}
- // const actions = detail?.codeActions;
- // const isImport = !!detail?.source;
-
- // TODO: handle actions
- // if (actions) {
- // const edit: TextEdit[] = [];
-
- // for (const action of actions) {
- // for (const change of action.changes) {
- // edit.push(
- // ...this.codeActionChangesToTextEdit(
- // document,
- // fragment,
- // change,
- // isImport,
- // isInsideFrontmatter(fragment.getFullText(), fragment.offsetAt(comp.position))
- // )
- // );
- // }
- // }
-
- // completionItem.additionalTextEdits = edit;
- // }
-
return completionItem;
}
diff --git a/tools/language-server/src/plugins/typescript/features/HoverProvider.ts b/tools/language-server/src/plugins/typescript/features/HoverProvider.ts
new file mode 100644
index 000000000..2757ed4cd
--- /dev/null
+++ b/tools/language-server/src/plugins/typescript/features/HoverProvider.ts
@@ -0,0 +1,42 @@
+import type { LanguageServiceManager } from '../LanguageServiceManager';
+import ts from 'typescript';
+import { Hover, Position } from 'vscode-languageserver';
+import { Document, mapObjWithRangeToOriginal } from '../../../core/documents';
+import { HoverProvider } from '../../interfaces';
+import { getMarkdownDocumentation } from '../previewer';
+import { convertRange, toVirtualAstroFilePath } from '../utils';
+
+export class HoverProviderImpl implements HoverProvider {
+ constructor(private readonly lang: LanguageServiceManager) {}
+
+ async doHover(document: Document, position: Position): Promise<Hover | null> {
+ const { lang, tsDoc } = await this.getLSAndTSDoc(document);
+ const fragment = await tsDoc.getFragment();
+
+ const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
+ const filePath = toVirtualAstroFilePath(tsDoc.filePath);
+ let info = lang.getQuickInfoAtPosition(filePath, offset);
+ if (!info) {
+ return null;
+ }
+
+ const textSpan = info.textSpan;
+
+ const declaration = ts.displayPartsToString(info.displayParts);
+ const documentation = getMarkdownDocumentation(info.documentation, info.tags);
+
+ // https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
+ const contents = ['```typescript', declaration, '```']
+ .concat(documentation ? ['---', documentation] : [])
+ .join('\n');
+
+ return mapObjWithRangeToOriginal(fragment, {
+ range: convertRange(fragment, textSpan),
+ contents
+ });
+ }
+
+ private async getLSAndTSDoc(document: Document) {
+ return this.lang.getTypeScriptDoc(document);
+ }
+} \ No newline at end of file
diff --git a/tools/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts b/tools/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts
new file mode 100644
index 000000000..1be286246
--- /dev/null
+++ b/tools/language-server/src/plugins/typescript/features/SignatureHelpProvider.ts
@@ -0,0 +1,158 @@
+import type { LanguageServiceManager } from '../LanguageServiceManager';
+import type { SignatureHelpProvider } from '../../interfaces';
+import ts from 'typescript';
+import {
+ Position,
+ SignatureHelpContext,
+ SignatureHelp,
+ SignatureHelpTriggerKind,
+ SignatureInformation,
+ ParameterInformation,
+ MarkupKind,
+ CancellationToken
+} from 'vscode-languageserver';
+import { Document } from '../../../core/documents';
+import { getMarkdownDocumentation } from '../previewer';
+import { toVirtualAstroFilePath } from '../utils';
+
+export class SignatureHelpProviderImpl implements SignatureHelpProvider {
+ constructor(private readonly lang: LanguageServiceManager) {}
+
+ private static readonly triggerCharacters = ['(', ',', '<'];
+ private static readonly retriggerCharacters = [')'];
+
+ async getSignatureHelp(
+ document: Document,
+ position: Position,
+ context: SignatureHelpContext | undefined,
+ cancellationToken?: CancellationToken
+ ): Promise<SignatureHelp | null> {
+ const { lang, tsDoc } = await this.lang.getTypeScriptDoc(document);
+ const fragment = await tsDoc.getFragment();
+
+ if (cancellationToken?.isCancellationRequested) {
+ return null;
+ }
+
+ const offset = fragment.offsetAt(fragment.getGeneratedPosition(position));
+ const triggerReason = this.toTsTriggerReason(context);
+ const info = lang.getSignatureHelpItems(
+ toVirtualAstroFilePath(tsDoc.filePath),
+ offset,
+ triggerReason ? { triggerReason } : undefined
+ );
+ if (
+ !info ||
+ info.items.some((signature) => this.isInSvelte2tsxGeneratedFunction(signature))
+ ) {
+ return null;
+ }
+
+ const signatures = info.items.map(this.toSignatureHelpInformation);
+
+ return {
+ signatures,
+ activeSignature: info.selectedItemIndex,
+ activeParameter: info.argumentIndex
+ };
+ }
+
+ private isReTrigger(
+ isRetrigger: boolean,
+ triggerCharacter: string
+ ): triggerCharacter is ts.SignatureHelpRetriggerCharacter {
+ return (
+ isRetrigger &&
+ (this.isTriggerCharacter(triggerCharacter) ||
+ SignatureHelpProviderImpl.retriggerCharacters.includes(triggerCharacter))
+ );
+ }
+
+ private isTriggerCharacter(
+ triggerCharacter: string
+ ): triggerCharacter is ts.SignatureHelpTriggerCharacter {
+ return SignatureHelpProviderImpl.triggerCharacters.includes(triggerCharacter);
+ }
+
+ /**
+ * adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L103
+ */
+ private toTsTriggerReason(
+ context: SignatureHelpContext | undefined
+ ): ts.SignatureHelpTriggerReason {
+ switch (context?.triggerKind) {
+ case SignatureHelpTriggerKind.TriggerCharacter:
+ if (context.triggerCharacter) {
+ if (this.isReTrigger(context.isRetrigger, context.triggerCharacter)) {
+ return { kind: 'retrigger', triggerCharacter: context.triggerCharacter };
+ }
+ if (this.isTriggerCharacter(context.triggerCharacter)) {
+ return {
+ kind: 'characterTyped',
+ triggerCharacter: context.triggerCharacter
+ };
+ }
+ }
+ return { kind: 'invoked' };
+ case SignatureHelpTriggerKind.ContentChange:
+ return context.isRetrigger ? { kind: 'retrigger' } : { kind: 'invoked' };
+
+ case SignatureHelpTriggerKind.Invoked:
+ default:
+ return { kind: 'invoked' };
+ }
+ }
+
+ /**
+ * adopted from https://github.com/microsoft/vscode/blob/265a2f6424dfbd3a9788652c7d376a7991d049a3/extensions/typescript-language-features/src/languageFeatures/signatureHelp.ts#L73
+ */
+ private toSignatureHelpInformation(item: ts.SignatureHelpItem): SignatureInformation {
+ const [prefixLabel, separatorLabel, suffixLabel] = [
+ item.prefixDisplayParts,
+ item.separatorDisplayParts,
+ item.suffixDisplayParts
+ ].map(ts.displayPartsToString);
+
+ let textIndex = prefixLabel.length;
+ let signatureLabel = '';
+ const parameters: ParameterInformation[] = [];
+ const lastIndex = item.parameters.length - 1;
+
+ item.parameters.forEach((parameter, index) => {
+ const label = ts.displayPartsToString(parameter.displayParts);
+
+ const startIndex = textIndex;
+ const endIndex = textIndex + label.length;
+ const doc = ts.displayPartsToString(parameter.documentation);
+
+ signatureLabel += label;
+ parameters.push(ParameterInformation.create([startIndex, endIndex], doc));
+
+ if (index < lastIndex) {
+ textIndex = endIndex + separatorLabel.length;
+ signatureLabel += separatorLabel;
+ }
+ });
+ const signatureDocumentation = getMarkdownDocumentation(
+ item.documentation,
+ item.tags.filter((tag) => tag.name !== 'param')
+ );
+
+ return {
+ label: prefixLabel + signatureLabel + suffixLabel,
+ documentation: signatureDocumentation
+ ? {
+ value: signatureDocumentation,
+ kind: MarkupKind.Markdown
+ }
+ : undefined,
+ parameters
+ };
+ }
+
+ private isInSvelte2tsxGeneratedFunction(signatureHelpItem: ts.SignatureHelpItem) {
+ return signatureHelpItem.prefixDisplayParts.some((part) =>
+ part.text.includes('__sveltets')
+ );
+ }
+} \ No newline at end of file
diff --git a/tools/language-server/src/plugins/typescript/previewer.ts b/tools/language-server/src/plugins/typescript/previewer.ts
new file mode 100644
index 000000000..deedae1e8
--- /dev/null
+++ b/tools/language-server/src/plugins/typescript/previewer.ts
@@ -0,0 +1,140 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+/**
+ * adopted from https://github.com/microsoft/vscode/blob/10722887b8629f90cc38ee7d90d54e8246dc895f/extensions/typescript-language-features/src/utils/previewer.ts
+ */
+
+ import ts from 'typescript';
+ import { isNotNullOrUndefined } from '../../utils';
+
+ function replaceLinks(text: string): string {
+ return (
+ text
+ // Http(s) links
+ .replace(
+ /\{@(link|linkplain|linkcode) (https?:\/\/[^ |}]+?)(?:[| ]([^{}\n]+?))?\}/gi,
+ (_, tag: string, link: string, text?: string) => {
+ switch (tag) {
+ case 'linkcode':
+ return `[\`${text ? text.trim() : link}\`](${link})`;
+
+ default:
+ return `[${text ? text.trim() : link}](${link})`;
+ }
+ }
+ )
+ );
+ }
+
+ function processInlineTags(text: string): string {
+ return replaceLinks(text);
+ }
+
+ function getTagBodyText(tag: ts.JSDocTagInfo): string | undefined {
+ if (!tag.text) {
+ return undefined;
+ }
+
+ // Convert to markdown code block if it is not already one
+ function makeCodeblock(text: string): string {
+ if (text.match(/^\s*[~`]{3}/g)) {
+ return text;
+ }
+ return '```\n' + text + '\n```';
+ }
+
+ function makeExampleTag(text: string) {
+ // check for caption tags, fix for https://github.com/microsoft/vscode/issues/79704
+ const captionTagMatches = text.match(/<caption>(.*?)<\/caption>\s*(\r\n|\n)/);
+ if (captionTagMatches && captionTagMatches.index === 0) {
+ return (
+ captionTagMatches[1] +
+ '\n\n' +
+ makeCodeblock(text.substr(captionTagMatches[0].length))
+ );
+ } else {
+ return makeCodeblock(text);
+ }
+ }
+
+ function makeEmailTag(text: string) {
+ // fix obsucated email address, https://github.com/microsoft/vscode/issues/80898
+ const emailMatch = text.match(/(.+)\s<([-.\w]+@[-.\w]+)>/);
+
+ if (emailMatch === null) {
+ return text;
+ } else {
+ return `${emailMatch[1]} ${emailMatch[2]}`;
+ }
+ }
+
+ switch (tag.name) {
+ case 'example':
+ return makeExampleTag(ts.displayPartsToString(tag.text));
+ case 'author':
+ return makeEmailTag(ts.displayPartsToString(tag.text));
+ case 'default':
+ return makeCodeblock(ts.displayPartsToString(tag.text));
+ }
+
+ return processInlineTags(ts.displayPartsToString(tag.text));
+ }
+
+ export function getTagDocumentation(tag: ts.JSDocTagInfo): string | undefined {
+ function getWithType() {
+ const body = (ts.displayPartsToString(tag.text) || '').split(/^(\S+)\s*-?\s*/);
+ if (body?.length === 3) {
+ const param = body[1];
+ const doc = body[2];
+ const label = `*@${tag.name}* \`${param}\``;
+ if (!doc) {
+ return label;
+ }
+ return (
+ label +
+ (doc.match(/\r\n|\n/g)
+ ? ' \n' + processInlineTags(doc)
+ : ` — ${processInlineTags(doc)}`)
+ );
+ }
+ }
+
+ switch (tag.name) {
+ case 'augments':
+ case 'extends':
+ case 'param':
+ case 'template':
+ return getWithType();
+ }
+
+ // Generic tag
+ const label = `*@${tag.name}*`;
+ const text = getTagBodyText(tag);
+ if (!text) {
+ return label;
+ }
+ return label + (text.match(/\r\n|\n/g) ? ' \n' + text : ` — ${text}`);
+ }
+
+ export function plain(parts: ts.SymbolDisplayPart[] | string): string {
+ return processInlineTags(typeof parts === 'string' ? parts : ts.displayPartsToString(parts));
+ }
+
+ export function getMarkdownDocumentation(
+ documentation: ts.SymbolDisplayPart[] | undefined,
+ tags: ts.JSDocTagInfo[] | undefined
+ ) {
+ let result: Array<string | undefined> = [];
+ if (documentation) {
+ result.push(plain(documentation));
+ }
+
+ if (tags) {
+ result = result.concat(tags.map(getTagDocumentation));
+ }
+
+ return result.filter(isNotNullOrUndefined).join('\n\n');
+ } \ No newline at end of file
diff --git a/tools/language-server/src/plugins/typescript/utils.ts b/tools/language-server/src/plugins/typescript/utils.ts
index d84f35da1..4b767c8e1 100644
--- a/tools/language-server/src/plugins/typescript/utils.ts
+++ b/tools/language-server/src/plugins/typescript/utils.ts
@@ -178,6 +178,9 @@ export function isVirtualFilePath(filePath: string) {
}
export function toVirtualAstroFilePath(filePath: string) {
+ if(isVirtualFrameworkFilePath('astro', filePath)) {
+ return filePath;
+ }
return `${filePath}.ts`;
}