Fix GoToDefinition for component imports (and component usage) (#1045)
* Fix GoToDefinition for component imports (and component usage) * Remove extra comment
This commit is contained in:
parent
812c9d8269
commit
5293519470
4 changed files with 88 additions and 12 deletions
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
|
@ -7,8 +7,8 @@
|
||||||
"type": "extensionHost",
|
"type": "extensionHost",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"runtimeExecutable": "${execPath}",
|
"runtimeExecutable": "${execPath}",
|
||||||
"args": ["--extensionDevelopmentPath=${workspaceRoot}/tools/astro-vscode"],
|
"args": ["--extensionDevelopmentPath=${workspaceRoot}/tools/vscode"],
|
||||||
"outFiles": ["${workspaceRoot}/tools/astro-vscode/dist/**/*.js"]
|
"outFiles": ["${workspaceRoot}/tools/vscode/dist/**/*.js"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
"name": "Attach to Server",
|
"name": "Attach to Server",
|
||||||
"port": 6040,
|
"port": 6040,
|
||||||
"restart": true,
|
"restart": true,
|
||||||
"outFiles": ["${workspaceRoot}/tools/astro-languageserver/dist/**/*.js"],
|
"outFiles": ["${workspaceRoot}/tools/languageserver/dist/**/*.js"],
|
||||||
"skipFiles": ["<node_internals>/**"]
|
"skipFiles": ["<node_internals>/**"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { isInTag, positionAt, offsetAt } from '../../core/documents/utils';
|
||||||
import { pathToUrl } from '../../utils';
|
import { pathToUrl } from '../../utils';
|
||||||
import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } 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.
|
* The mapper to get from original snapshot positions to generated and vice versa.
|
||||||
*/
|
*/
|
||||||
|
@ -66,7 +68,15 @@ class AstroDocumentSnapshot implements DocumentSnapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
get text() {
|
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() {
|
get filePath() {
|
||||||
|
@ -128,7 +138,9 @@ export class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFrag
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
private transformContent(content: string) {
|
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) {
|
getText(start: number, end: number) {
|
||||||
|
@ -214,6 +226,10 @@ export class TypeScriptDocumentSnapshot implements DocumentSnapshot {
|
||||||
return this as unknown as any;
|
return this as unknown as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOriginalPosition(pos: Position): Position {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
destroyFragment() {
|
destroyFragment() {
|
||||||
// nothing to clean up
|
// nothing to clean up
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
|
import { join as pathJoin, dirname as pathDirname } from 'path';
|
||||||
import { Document, DocumentManager, isInsideFrontmatter } from '../../core/documents';
|
import { Document, DocumentManager, isInsideFrontmatter } from '../../core/documents';
|
||||||
import type { ConfigManager } from '../../core/config';
|
import type { ConfigManager } from '../../core/config';
|
||||||
import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces';
|
import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces';
|
||||||
|
import { SourceFile, ImportDeclaration, Node, SyntaxKind } from 'typescript';
|
||||||
import { CompletionContext, DefinitionLink, FileChangeType, Position, LocationLink } from 'vscode-languageserver';
|
import { CompletionContext, DefinitionLink, FileChangeType, Position, LocationLink } from 'vscode-languageserver';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider';
|
import { CompletionsProviderImpl, CompletionEntryWithIdentifer } from './features/CompletionsProvider';
|
||||||
import { LanguageServiceManager } from './LanguageServiceManager';
|
import { LanguageServiceManager } from './LanguageServiceManager';
|
||||||
import { SnapshotManager } from './SnapshotManager';
|
import { SnapshotManager } from './SnapshotManager';
|
||||||
import { convertToLocationRange, isVirtualFilePath, getScriptKindFromFileName } from './utils';
|
import { convertToLocationRange, isVirtualAstroFilePath, isVirtualFilePath, getScriptKindFromFileName } from './utils';
|
||||||
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
|
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
|
||||||
import { isNotNullOrUndefined, pathToUrl } from '../../utils';
|
import { isNotNullOrUndefined, pathToUrl } from '../../utils';
|
||||||
|
|
||||||
|
type BetterTS = typeof ts & {
|
||||||
|
getTouchingPropertyName(sourceFile: SourceFile, pos: number): Node;
|
||||||
|
};
|
||||||
|
|
||||||
export class TypeScriptPlugin implements CompletionsProvider {
|
export class TypeScriptPlugin implements CompletionsProvider {
|
||||||
private readonly docManager: DocumentManager;
|
private readonly docManager: DocumentManager;
|
||||||
private readonly configManager: ConfigManager;
|
private readonly configManager: ConfigManager;
|
||||||
|
@ -46,26 +52,38 @@ export class TypeScriptPlugin implements CompletionsProvider {
|
||||||
const filePath = tsDoc.filePath;
|
const filePath = tsDoc.filePath;
|
||||||
const tsFilePath = filePath.endsWith('.ts') ? filePath : filePath + '.ts';
|
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) {
|
if (!defs || !defs.definitions) {
|
||||||
return [];
|
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);
|
const docs = new SnapshotFragmentMap(this.languageServiceManager);
|
||||||
docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc });
|
docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc });
|
||||||
|
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
defs.definitions.map(async (def) => {
|
defs.definitions!.map(async (def) => {
|
||||||
const { fragment, snapshot } = await docs.retrieve(def.fileName);
|
const { fragment, snapshot } = await docs.retrieve(def.fileName);
|
||||||
|
|
||||||
if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
|
if (isNoTextSpanInGeneratedCode(snapshot.getFullText(), def.textSpan)) {
|
||||||
const fileName = isVirtualFilePath(def.fileName) ? def.fileName.substr(0, def.fileName.length - 3) : def.fileName;
|
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(
|
return LocationLink.create(
|
||||||
pathToUrl(fileName),
|
pathToUrl(fileName),
|
||||||
convertToLocationRange(fragment, def.textSpan),
|
convertToLocationRange(fragment, textSpan),
|
||||||
convertToLocationRange(fragment, def.textSpan),
|
convertToLocationRange(fragment, textSpan),
|
||||||
convertToLocationRange(mainFragment, defs.textSpan)
|
convertToLocationRange(mainFragment, defs!.textSpan)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -110,4 +128,46 @@ export class TypeScriptPlugin implements CompletionsProvider {
|
||||||
private isInsideFrontmatter(document: Document, position: Position) {
|
private isInsideFrontmatter(document: Document, position: Position) {
|
||||||
return isInsideFrontmatter(document.getText(), document.offsetAt(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -190,7 +190,7 @@ export function ensureRealAstroFilePath(filePath: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureRealFilePath(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[]) {
|
export function findTsConfigPath(fileName: string, rootUris: string[]) {
|
||||||
|
|
Loading…
Reference in a new issue