summaryrefslogtreecommitdiff
path: root/tools/language-server/src
diff options
context:
space:
mode:
Diffstat (limited to 'tools/language-server/src')
-rw-r--r--tools/language-server/src/plugins/typescript/DocumentSnapshot.ts20
-rw-r--r--tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts72
-rw-r--r--tools/language-server/src/plugins/typescript/utils.ts2
3 files changed, 85 insertions, 9 deletions
diff --git a/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts b/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts
index 9e2e778c6..606db8d58 100644
--- a/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts
+++ b/tools/language-server/src/plugins/typescript/DocumentSnapshot.ts
@@ -5,6 +5,8 @@ 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 ''; };`;
+
/**
* The mapper to get from original snapshot positions to generated and vice versa.
*/
@@ -66,7 +68,15 @@ class AstroDocumentSnapshot implements DocumentSnapshot {
}
get text() {
- return this.doc.getText();
+ let raw = this.doc.getText();
+ return this.transformContent(raw);
+ }
+
+ /** @internal */
+ private transformContent(content: string) {
+ return content.replace(/---/g, '///') +
+ // TypeScript needs this to know there's a default export.
+ FILLER_DEFAULT_EXPORT;
}
get filePath() {
@@ -128,7 +138,9 @@ export class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFrag
/** @internal */
private transformContent(content: string) {
- return content.replace(/---/g, '///');
+ return content.replace(/---/g, '///') +
+ // TypeScript needs this to know there's a default export.
+ FILLER_DEFAULT_EXPORT;
}
getText(start: number, end: number) {
@@ -214,6 +226,10 @@ export class TypeScriptDocumentSnapshot implements DocumentSnapshot {
return this as unknown as any;
}
+ getOriginalPosition(pos: Position): Position {
+ return pos;
+ }
+
destroyFragment() {
// nothing to clean up
}
diff --git a/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts
index 10b94cb83..918856fb8 100644
--- a/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts
+++ b/tools/language-server/src/plugins/typescript/TypeScriptPlugin.ts
@@ -1,15 +1,21 @@
+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 { 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, isVirtualFilePath, getScriptKindFromFileName } from './utils';
+import { convertToLocationRange, isVirtualAstroFilePath, isVirtualFilePath, getScriptKindFromFileName } from './utils';
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
import { isNotNullOrUndefined, pathToUrl } from '../../utils';
+type BetterTS = typeof ts & {
+ getTouchingPropertyName(sourceFile: SourceFile, pos: number): Node;
+};
+
export class TypeScriptPlugin implements CompletionsProvider {
private readonly docManager: DocumentManager;
private readonly configManager: ConfigManager;
@@ -46,26 +52,38 @@ export class TypeScriptPlugin implements CompletionsProvider {
const filePath = tsDoc.filePath;
const tsFilePath = filePath.endsWith('.ts') ? filePath : filePath + '.ts';
- const defs = lang.getDefinitionAndBoundSpan(tsFilePath, mainFragment.offsetAt(mainFragment.getGeneratedPosition(position)));
+ const fragmentPosition = mainFragment.getGeneratedPosition(position);
+ const fragmentOffset = mainFragment.offsetAt(fragmentPosition);
+
+ let defs = lang.getDefinitionAndBoundSpan(tsFilePath, fragmentOffset);
if (!defs || !defs.definitions) {
return [];
}
+ // Resolve all imports if we can
+ if(this.goToDefinitionFoundOnlyAlias(tsFilePath, defs.definitions!)) {
+ let importDef = this.getGoToDefinitionRefsForImportSpecifier(tsFilePath, fragmentOffset, lang);
+ if(importDef) {
+ defs = importDef;
+ }
+ }
+
const docs = new SnapshotFragmentMap(this.languageServiceManager);
docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc });
const result = await Promise.all(
- defs.definitions.map(async (def) => {
+ defs.definitions!.map(async (def) => {
const { fragment, snapshot } = await docs.retrieve(def.fileName);
if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
const fileName = isVirtualFilePath(def.fileName) ? def.fileName.substr(0, def.fileName.length - 3) : def.fileName;
+ const textSpan = isVirtualAstroFilePath(tsFilePath) ? { start: 0, length: 0 } : def.textSpan;
return LocationLink.create(
pathToUrl(fileName),
- convertToLocationRange(fragment, def.textSpan),
- convertToLocationRange(fragment, def.textSpan),
- convertToLocationRange(mainFragment, defs.textSpan)
+ convertToLocationRange(fragment, textSpan),
+ convertToLocationRange(fragment, textSpan),
+ convertToLocationRange(mainFragment, defs!.textSpan)
);
}
})
@@ -110,4 +128,46 @@ export class TypeScriptPlugin implements CompletionsProvider {
private isInsideFrontmatter(document: Document, position: Position) {
return isInsideFrontmatter(document.getText(), document.offsetAt(position));
}
+
+ private goToDefinitionFoundOnlyAlias(tsFileName: string, defs: readonly ts.DefinitionInfo[]) {
+ return !!(defs.length === 1 &&
+ defs[0].kind === 'alias' &&
+ defs[0].fileName === tsFileName);
+ }
+
+ private getGoToDefinitionRefsForImportSpecifier(tsFilePath: string, offset: number, lang: ts.LanguageService): ts.DefinitionInfoAndBoundSpan | undefined {
+ const program = lang.getProgram();
+ const sourceFile = program?.getSourceFile(tsFilePath);
+ if (sourceFile) {
+ let node = (ts as BetterTS).getTouchingPropertyName(sourceFile, offset);
+ if(node && node.kind === SyntaxKind.Identifier) {
+ if(node.parent.kind === SyntaxKind.ImportClause) {
+ let decl = node.parent.parent as ImportDeclaration;
+ let spec = ts.isStringLiteral(decl.moduleSpecifier) && decl.moduleSpecifier.text;
+ if(spec) {
+ let fileName = pathJoin(pathDirname(tsFilePath), spec);
+ let start = node.pos + 1;
+ let def: ts.DefinitionInfoAndBoundSpan = {
+ definitions: [{
+ kind: 'alias',
+ fileName,
+ name: '',
+ containerKind: '',
+ containerName: '',
+ textSpan: {
+ start: 0,
+ length: 0
+ }
+ } as ts.DefinitionInfo],
+ textSpan: {
+ start,
+ length: node.end - start
+ }
+ };
+ return def;
+ }
+ }
+ }
+ }
+ }
}
diff --git a/tools/language-server/src/plugins/typescript/utils.ts b/tools/language-server/src/plugins/typescript/utils.ts
index 9acbe2ed8..d84f35da1 100644
--- a/tools/language-server/src/plugins/typescript/utils.ts
+++ b/tools/language-server/src/plugins/typescript/utils.ts
@@ -190,7 +190,7 @@ export function ensureRealAstroFilePath(filePath: string) {
}
export function ensureRealFilePath(filePath: string) {
- return isVirtualFilePath(filePath) ? filePath.slice(0, 3) : filePath;
+ return isVirtualFilePath(filePath) ? filePath.slice(0, filePath.length - 3) : filePath;
}
export function findTsConfigPath(fileName: string, rootUris: string[]) {