diff options
author | 2021-08-10 09:30:02 -0400 | |
---|---|---|
committer | 2021-08-10 09:30:02 -0400 | |
commit | 2c5380a26631f720bfbbcec78295c71b562e647d (patch) | |
tree | 40ead718c2d817265a8d40bb04d099aae507c4a7 | |
parent | 1339d5e36bdaae1eeea6bf9d99b2bdf4d69d604a (diff) | |
download | astro-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
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`; } |