Add support for Astro.* completion and Hover help (#1068)
* Add support for Astro.* completion and Hover help * Allow providing a generic type to fetchContent
This commit is contained in:
parent
1339d5e36b
commit
2c5380a266
15 changed files with 495 additions and 61 deletions
27
tools/language-server/astro.d.ts
vendored
Normal file
27
tools/language-server/astro.d.ts
vendored
Normal file
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
);
|
||||
}
|
||||
}
|
140
tools/language-server/src/plugins/typescript/previewer.ts
Normal file
140
tools/language-server/src/plugins/typescript/previewer.ts
Normal file
|
@ -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');
|
||||
}
|
|
@ -178,6 +178,9 @@ export function isVirtualFilePath(filePath: string) {
|
|||
}
|
||||
|
||||
export function toVirtualAstroFilePath(filePath: string) {
|
||||
if(isVirtualFrameworkFilePath('astro', filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
return `${filePath}.ts`;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue