Get definitions (#219)
* Start on css completion * Support for CSS completions * Adds support for Go to Definition in TypeScript in Astro * Run formatting
This commit is contained in:
parent
3da2b58b9d
commit
6ce068b838
11 changed files with 430 additions and 219 deletions
|
@ -1,5 +1,6 @@
|
|||
import { HTMLDocument, Node, Position } from 'vscode-html-languageservice';
|
||||
import { clamp } from '../../utils';
|
||||
import { Range } from 'vscode-languageserver';
|
||||
import { clamp, isInRange } from '../../utils';
|
||||
import { parseHtml } from './parseHtml';
|
||||
|
||||
export interface TagInformation {
|
||||
|
@ -84,6 +85,10 @@ export function isInsideFrontmatter(text: string, offset: number): boolean {
|
|||
return start > 1 && start < 3 && end >= 1;
|
||||
}
|
||||
|
||||
export function isInTag(position: Position, tagInfo: TagInformation | null): tagInfo is TagInformation {
|
||||
return !!tagInfo && isInRange(position, Range.create(tagInfo.startPos, tagInfo.endPos));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the line and character based on the offset
|
||||
* @param offset The index of the position
|
||||
|
|
|
@ -19,6 +19,10 @@ export function startServer() {
|
|||
connection.onInitialize((evt) => {
|
||||
const workspaceUris = evt.workspaceFolders?.map((folder) => folder.uri.toString()) ?? [evt.rootUri ?? ''];
|
||||
|
||||
pluginHost.initialize({
|
||||
filterIncompleteCompletions: !evt.initializationOptions?.dontFilterIncompleteCompletions,
|
||||
definitionLinkSupport: !!evt.capabilities.textDocument?.definition?.linkSupport,
|
||||
});
|
||||
pluginHost.register(new AstroPlugin(docManager, configManager));
|
||||
pluginHost.register(new HTMLPlugin(docManager, configManager));
|
||||
pluginHost.register(new CSSPlugin(docManager, configManager));
|
||||
|
@ -29,6 +33,7 @@ export function startServer() {
|
|||
capabilities: {
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
foldingRangeProvider: true,
|
||||
definitionProvider: true,
|
||||
completionProvider: {
|
||||
resolveProvider: true,
|
||||
triggerCharacters: [
|
||||
|
@ -102,6 +107,7 @@ export function startServer() {
|
|||
|
||||
return pluginHost.resolveCompletion(data, completionItem);
|
||||
});
|
||||
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));
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CompletionContext, CompletionItem, CompletionList, Position, TextDocumentIdentifier } from 'vscode-languageserver';
|
||||
import { CompletionContext, CompletionItem, CompletionList, DefinitionLink, Location, Position, TextDocumentIdentifier } from 'vscode-languageserver';
|
||||
import type { DocumentManager } from '../core/documents';
|
||||
import type * as d from './interfaces';
|
||||
import { flatten } from '../utils';
|
||||
|
@ -10,11 +10,24 @@ enum ExecuteMode {
|
|||
Collect,
|
||||
}
|
||||
|
||||
interface PluginHostConfig {
|
||||
filterIncompleteCompletions: boolean;
|
||||
definitionLinkSupport: boolean;
|
||||
}
|
||||
|
||||
export class PluginHost {
|
||||
private plugins: d.Plugin[] = [];
|
||||
private pluginHostConfig: PluginHostConfig = {
|
||||
filterIncompleteCompletions: true,
|
||||
definitionLinkSupport: false,
|
||||
};
|
||||
|
||||
constructor(private documentsManager: DocumentManager) {}
|
||||
|
||||
initialize(pluginHostConfig: PluginHostConfig) {
|
||||
this.pluginHostConfig = pluginHostConfig;
|
||||
}
|
||||
|
||||
register(plugin: d.Plugin) {
|
||||
this.plugins.push(plugin);
|
||||
}
|
||||
|
@ -67,6 +80,21 @@ export class PluginHost {
|
|||
return foldingRanges;
|
||||
}
|
||||
|
||||
async getDefinitions(textDocument: TextDocumentIdentifier, position: Position): Promise<DefinitionLink[] | Location[]> {
|
||||
const document = this.getDocument(textDocument.uri);
|
||||
if (!document) {
|
||||
throw new Error('Cannot call methods on an unopened document');
|
||||
}
|
||||
|
||||
const definitions = flatten(await this.execute<DefinitionLink[]>('getDefinitions', [document, position], ExecuteMode.Collect));
|
||||
|
||||
if (this.pluginHostConfig.definitionLinkSupport) {
|
||||
return definitions;
|
||||
} else {
|
||||
return definitions.map((def) => <Location>{ range: def.targetSelectionRange, uri: def.targetUri });
|
||||
}
|
||||
}
|
||||
|
||||
onWatchFileChanges(onWatchFileChangesParams: any[]): void {
|
||||
for (const support of this.plugins) {
|
||||
support.onWatchFileChanges?.(onWatchFileChangesParams);
|
||||
|
|
|
@ -0,0 +1,242 @@
|
|||
import * as ts from 'typescript';
|
||||
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';
|
||||
|
||||
/**
|
||||
* The mapper to get from original snapshot positions to generated and vice versa.
|
||||
*/
|
||||
export interface SnapshotFragment extends DocumentMapper {
|
||||
positionAt(offset: number): Position;
|
||||
offsetAt(position: Position): number;
|
||||
}
|
||||
|
||||
export interface DocumentSnapshot extends ts.IScriptSnapshot {
|
||||
version: number;
|
||||
filePath: string;
|
||||
scriptKind: ts.ScriptKind;
|
||||
positionAt(offset: number): Position;
|
||||
/**
|
||||
* Instantiates a source mapper.
|
||||
* `destroyFragment` needs to be called when
|
||||
* it's no longer needed / the class should be cleaned up
|
||||
* in order to prevent memory leaks.
|
||||
*/
|
||||
getFragment(): Promise<DocumentFragmentSnapshot>;
|
||||
/**
|
||||
* Needs to be called when source mapper
|
||||
* is no longer needed / the class should be cleaned up
|
||||
* in order to prevent memory leaks.
|
||||
*/
|
||||
destroyFragment(): void;
|
||||
/**
|
||||
* Convenience function for getText(0, getLength())
|
||||
*/
|
||||
getFullText(): string;
|
||||
}
|
||||
|
||||
export const createDocumentSnapshot = (filePath: string, currentText: string | null, createDocument?: (_filePath: string, text: string) => Document): DocumentSnapshot => {
|
||||
const text = currentText || (ts.sys.readFile(filePath) ?? '');
|
||||
|
||||
if (isAstroFilePath(filePath)) {
|
||||
if (!createDocument) throw new Error('Astro documents require the "createDocument" utility to be provided');
|
||||
const snapshot = new AstroDocumentSnapshot(createDocument(filePath, text));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
return new TypeScriptDocumentSnapshot(0, filePath, text);
|
||||
};
|
||||
|
||||
class AstroDocumentSnapshot implements DocumentSnapshot {
|
||||
version = this.doc.version;
|
||||
scriptKind = ts.ScriptKind.Unknown;
|
||||
|
||||
constructor(private doc: Document) {}
|
||||
|
||||
async getFragment(): Promise<DocumentFragmentSnapshot> {
|
||||
const uri = pathToUrl(this.filePath);
|
||||
const mapper = await this.getMapper(uri);
|
||||
return new DocumentFragmentSnapshot(mapper, this.doc);
|
||||
}
|
||||
|
||||
async destroyFragment() {
|
||||
return;
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this.doc.getText();
|
||||
}
|
||||
|
||||
get filePath() {
|
||||
return this.doc.getFilePath() || '';
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
offsetAt(position: Position) {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
|
||||
private getMapper(uri: string) {
|
||||
return new IdentityMapper(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment' | 'destroyFragment'>, SnapshotFragment {
|
||||
version: number;
|
||||
filePath: string;
|
||||
url: string;
|
||||
text: string;
|
||||
|
||||
scriptKind = ts.ScriptKind.TSX;
|
||||
scriptInfo = null;
|
||||
|
||||
constructor(private mapper: any, private parent: Document) {
|
||||
const filePath = parent.getFilePath();
|
||||
if (!filePath) throw new Error('Cannot create a document fragment from a non-local document');
|
||||
const text = parent.getText();
|
||||
this.version = parent.version;
|
||||
this.filePath = toVirtualAstroFilePath(filePath);
|
||||
this.url = toVirtualAstroFilePath(filePath);
|
||||
this.text = this.transformContent(text);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private transformContent(content: string) {
|
||||
return content.replace(/---/g, '///');
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
offsetAt(position: Position): number {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
|
||||
getOriginalPosition(pos: Position): Position {
|
||||
return this.mapper.getOriginalPosition(pos);
|
||||
}
|
||||
|
||||
getGeneratedPosition(pos: Position): Position {
|
||||
return this.mapper.getGeneratedPosition(pos);
|
||||
}
|
||||
|
||||
isInGenerated(pos: Position): boolean {
|
||||
return !isInTag(pos, this.parent.styleInfo);
|
||||
}
|
||||
|
||||
getURL(): string {
|
||||
return this.url;
|
||||
}
|
||||
}
|
||||
|
||||
export class TypeScriptDocumentSnapshot implements DocumentSnapshot {
|
||||
scriptKind = getScriptKindFromFileName(this.filePath);
|
||||
scriptInfo = null;
|
||||
url: string;
|
||||
|
||||
constructor(public version: number, public readonly filePath: string, private text: string) {
|
||||
this.url = pathToUrl(filePath);
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
offsetAt(position: Position): number {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
|
||||
async getFragment(): Promise<DocumentFragmentSnapshot> {
|
||||
return (this as unknown) as any;
|
||||
}
|
||||
|
||||
destroyFragment() {
|
||||
// nothing to clean up
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
update(changes: TextDocumentContentChangeEvent[]): void {
|
||||
for (const change of changes) {
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
if ('range' in change) {
|
||||
start = this.offsetAt(change.range.start);
|
||||
end = this.offsetAt(change.range.end);
|
||||
} else {
|
||||
end = this.getLength();
|
||||
}
|
||||
|
||||
this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
|
||||
}
|
||||
|
||||
this.version++;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,8 @@ import type { Document, DocumentManager } from '../../core/documents';
|
|||
import type { ConfigManager } from '../../core/config';
|
||||
import { urlToPath, pathToUrl, debounceSameArg } from '../../utils';
|
||||
import { getLanguageService, getLanguageServiceForDocument, LanguageServiceContainer, LanguageServiceDocumentContext } from './languageService';
|
||||
import { DocumentSnapshot, SnapshotManager } from './SnapshotManager';
|
||||
import { SnapshotManager } from './SnapshotManager';
|
||||
import { DocumentSnapshot } from './DocumentSnapshot';
|
||||
|
||||
export class LanguageServiceManager {
|
||||
private readonly docManager: DocumentManager;
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import * as ts from 'typescript';
|
||||
import { TextDocumentContentChangeEvent, Position } from 'vscode-languageserver';
|
||||
import { Document } from '../../core/documents';
|
||||
import { positionAt, offsetAt } from '../../core/documents/utils';
|
||||
import { pathToUrl } from '../../utils';
|
||||
import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils';
|
||||
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
|
||||
import { toVirtualAstroFilePath } from './utils';
|
||||
import { DocumentSnapshot, TypeScriptDocumentSnapshot, createDocumentSnapshot } from './DocumentSnapshot';
|
||||
|
||||
export interface TsFilesSpec {
|
||||
include?: readonly string[];
|
||||
|
@ -95,209 +93,3 @@ export class SnapshotManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DocumentSnapshot extends ts.IScriptSnapshot {
|
||||
version: number;
|
||||
filePath: string;
|
||||
scriptKind: ts.ScriptKind;
|
||||
positionAt(offset: number): Position;
|
||||
/**
|
||||
* Instantiates a source mapper.
|
||||
* `destroyFragment` needs to be called when
|
||||
* it's no longer needed / the class should be cleaned up
|
||||
* in order to prevent memory leaks.
|
||||
*/
|
||||
getFragment(): Promise<DocumentFragmentSnapshot>;
|
||||
/**
|
||||
* Needs to be called when source mapper
|
||||
* is no longer needed / the class should be cleaned up
|
||||
* in order to prevent memory leaks.
|
||||
*/
|
||||
destroyFragment(): void;
|
||||
/**
|
||||
* Convenience function for getText(0, getLength())
|
||||
*/
|
||||
getFullText(): string;
|
||||
}
|
||||
|
||||
export const createDocumentSnapshot = (filePath: string, currentText: string | null, createDocument?: (_filePath: string, text: string) => Document): DocumentSnapshot => {
|
||||
const text = currentText || (ts.sys.readFile(filePath) ?? '');
|
||||
|
||||
if (isAstroFilePath(filePath)) {
|
||||
if (!createDocument) throw new Error('Astro documents require the "createDocument" utility to be provided');
|
||||
const snapshot = new AstroDocumentSnapshot(createDocument(filePath, text));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
return new TypeScriptDocumentSnapshot(0, filePath, text);
|
||||
};
|
||||
|
||||
class AstroDocumentSnapshot implements DocumentSnapshot {
|
||||
version = this.doc.version;
|
||||
scriptKind = ts.ScriptKind.Unknown;
|
||||
|
||||
constructor(private doc: Document) {}
|
||||
|
||||
async getFragment(): Promise<DocumentFragmentSnapshot> {
|
||||
return new DocumentFragmentSnapshot(this.doc);
|
||||
}
|
||||
|
||||
async destroyFragment() {
|
||||
return;
|
||||
}
|
||||
|
||||
get text() {
|
||||
return this.doc.getText();
|
||||
}
|
||||
|
||||
get filePath() {
|
||||
return this.doc.getFilePath() || '';
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
offsetAt(position: Position) {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentFragmentSnapshot implements Omit<DocumentSnapshot, 'getFragment' | 'destroyFragment'> {
|
||||
version: number;
|
||||
filePath: string;
|
||||
url: string;
|
||||
text: string;
|
||||
|
||||
scriptKind = ts.ScriptKind.TSX;
|
||||
scriptInfo = null;
|
||||
|
||||
constructor(private doc: Document) {
|
||||
const filePath = doc.getFilePath();
|
||||
if (!filePath) throw new Error('Cannot create a document fragment from a non-local document');
|
||||
const text = doc.getText();
|
||||
this.version = doc.version;
|
||||
this.filePath = toVirtualAstroFilePath(filePath);
|
||||
this.url = toVirtualAstroFilePath(filePath);
|
||||
this.text = this.transformContent(text);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private transformContent(content: string) {
|
||||
return content.replace(/---/g, '///');
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
offsetAt(position: Position): number {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
}
|
||||
|
||||
class TypeScriptDocumentSnapshot implements DocumentSnapshot {
|
||||
scriptKind = getScriptKindFromFileName(this.filePath);
|
||||
scriptInfo = null;
|
||||
url: string;
|
||||
|
||||
constructor(public version: number, public readonly filePath: string, private text: string) {
|
||||
this.url = pathToUrl(filePath);
|
||||
}
|
||||
|
||||
getText(start: number, end: number) {
|
||||
return this.text.substring(start, end);
|
||||
}
|
||||
|
||||
getLength() {
|
||||
return this.text.length;
|
||||
}
|
||||
|
||||
getFullText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
getChangeRange() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
positionAt(offset: number) {
|
||||
return positionAt(offset, this.text);
|
||||
}
|
||||
|
||||
offsetAt(position: Position): number {
|
||||
return offsetAt(position, this.text);
|
||||
}
|
||||
|
||||
async getFragment(): Promise<DocumentFragmentSnapshot> {
|
||||
return (this as unknown) as any;
|
||||
}
|
||||
|
||||
destroyFragment() {
|
||||
// nothing to clean up
|
||||
}
|
||||
|
||||
getLineContainingOffset(offset: number) {
|
||||
const chunks = this.getText(0, offset).split('\n');
|
||||
return chunks[chunks.length - 1];
|
||||
}
|
||||
|
||||
update(changes: TextDocumentContentChangeEvent[]): void {
|
||||
for (const change of changes) {
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
if ('range' in change) {
|
||||
start = this.offsetAt(change.range.start);
|
||||
end = this.offsetAt(change.range.end);
|
||||
} else {
|
||||
end = this.getLength();
|
||||
}
|
||||
|
||||
this.text = this.text.slice(0, start) + change.text + this.text.slice(end);
|
||||
}
|
||||
|
||||
this.version++;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import type { Document, DocumentManager } from '../../core/documents';
|
||||
import type { ConfigManager } from '../../core/config';
|
||||
import type { CompletionsProvider, AppCompletionItem, AppCompletionList } from '../interfaces';
|
||||
import { CompletionContext, Position, FileChangeType } from 'vscode-languageserver';
|
||||
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 { getScriptKindFromFileName } from './utils';
|
||||
import { convertToLocationRange, isVirtualFilePath, getScriptKindFromFileName } from './utils';
|
||||
import { isNoTextSpanInGeneratedCode, SnapshotFragmentMap } from './features/utils';
|
||||
import { isNotNullOrUndefined, pathToUrl } from '../../utils';
|
||||
|
||||
export class TypeScriptPlugin implements CompletionsProvider {
|
||||
private readonly docManager: DocumentManager;
|
||||
|
@ -33,6 +35,40 @@ export class TypeScriptPlugin implements CompletionsProvider {
|
|||
return this.completionProvider.resolveCompletion(document, completionItem);
|
||||
}
|
||||
|
||||
async getDefinitions(document: Document, position: Position): Promise<DefinitionLink[]> {
|
||||
const { lang, tsDoc } = await this.languageServiceManager.getTypeScriptDoc(document);
|
||||
const mainFragment = await tsDoc.getFragment();
|
||||
|
||||
const filePath = tsDoc.filePath;
|
||||
const tsFilePath = filePath.endsWith('.ts') ? filePath : filePath + '.ts';
|
||||
|
||||
const defs = lang.getDefinitionAndBoundSpan(tsFilePath, mainFragment.offsetAt(mainFragment.getGeneratedPosition(position)));
|
||||
|
||||
if (!defs || !defs.definitions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const docs = new SnapshotFragmentMap(this.languageServiceManager);
|
||||
docs.set(tsDoc.filePath, { fragment: mainFragment, snapshot: tsDoc });
|
||||
|
||||
const result = await Promise.all(
|
||||
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;
|
||||
return LocationLink.create(
|
||||
pathToUrl(fileName),
|
||||
convertToLocationRange(fragment, def.textSpan),
|
||||
convertToLocationRange(fragment, def.textSpan),
|
||||
convertToLocationRange(mainFragment, defs.textSpan)
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
return result.filter(isNotNullOrUndefined);
|
||||
}
|
||||
|
||||
async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> {
|
||||
const doneUpdateProjectFiles = new Set<SnapshotManager>();
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import type { SnapshotFragment, DocumentSnapshot } from '../DocumentSnapshot';
|
||||
import type { LanguageServiceManager } from '../LanguageServiceManager';
|
||||
|
||||
/**
|
||||
* Checks if this a section that should be completely ignored
|
||||
* because it's purely generated.
|
||||
*/
|
||||
export function isInGeneratedCode(text: string, start: number, end: number) {
|
||||
const lineStart = text.lastIndexOf('\n', start);
|
||||
const lineEnd = text.indexOf('\n', end);
|
||||
const lastStart = text.substring(lineStart, start).lastIndexOf('/*Ωignore_startΩ*/');
|
||||
const lastEnd = text.substring(lineStart, start).lastIndexOf('/*Ωignore_endΩ*/');
|
||||
return lastStart > lastEnd && text.substring(end, lineEnd).includes('/*Ωignore_endΩ*/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that this isn't a text span that should be completely ignored
|
||||
* because it's purely generated.
|
||||
*/
|
||||
export function isNoTextSpanInGeneratedCode(text: string, span: ts.TextSpan) {
|
||||
return !isInGeneratedCode(text, span.start, span.start + span.length);
|
||||
}
|
||||
|
||||
export class SnapshotFragmentMap {
|
||||
private map = new Map<string, { fragment: SnapshotFragment; snapshot: DocumentSnapshot }>();
|
||||
constructor(private languageServiceManager: LanguageServiceManager) {}
|
||||
|
||||
set(fileName: string, content: { fragment: SnapshotFragment; snapshot: DocumentSnapshot }) {
|
||||
this.map.set(fileName, content);
|
||||
}
|
||||
|
||||
get(fileName: string) {
|
||||
return this.map.get(fileName);
|
||||
}
|
||||
|
||||
getFragment(fileName: string) {
|
||||
return this.map.get(fileName)?.fragment;
|
||||
}
|
||||
|
||||
async retrieve(fileName: string) {
|
||||
let snapshotFragment = this.get(fileName);
|
||||
if (!snapshotFragment) {
|
||||
const snapshot = await this.languageServiceManager.getSnapshot(fileName);
|
||||
const fragment = await snapshot.getFragment();
|
||||
snapshotFragment = { fragment, snapshot };
|
||||
this.set(fileName, snapshotFragment);
|
||||
}
|
||||
return snapshotFragment;
|
||||
}
|
||||
|
||||
async retrieveFragment(fileName: string) {
|
||||
return (await this.retrieve(fileName)).fragment;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,8 @@ import * as ts from 'typescript';
|
|||
import { basename } from 'path';
|
||||
import { ensureRealAstroFilePath, findTsConfigPath } from './utils';
|
||||
import { Document } from '../../core/documents';
|
||||
import { createDocumentSnapshot, SnapshotManager, DocumentSnapshot } from './SnapshotManager';
|
||||
import { SnapshotManager } from './SnapshotManager';
|
||||
import { createDocumentSnapshot, DocumentSnapshot } from './DocumentSnapshot';
|
||||
import { createAstroModuleLoader } from './module-loader';
|
||||
|
||||
const services = new Map<string, Promise<LanguageServiceContainer>>();
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import * as ts from 'typescript';
|
||||
import { CompletionItemKind, DiagnosticSeverity } from 'vscode-languageserver';
|
||||
import { CompletionItemKind, DiagnosticSeverity, Position, Range } from 'vscode-languageserver';
|
||||
import { dirname } from 'path';
|
||||
import { pathToUrl } from '../../utils';
|
||||
import { mapRangeToOriginal } from '../../core/documents';
|
||||
import { SnapshotFragment } from './DocumentSnapshot';
|
||||
|
||||
export function scriptElementKindToCompletionItemKind(kind: ts.ScriptElementKind): CompletionItemKind {
|
||||
switch (kind) {
|
||||
|
@ -127,12 +129,52 @@ export function getExtensionFromScriptKind(kind: ts.ScriptKind | undefined): ts.
|
|||
}
|
||||
}
|
||||
|
||||
export function convertRange(document: { positionAt: (offset: number) => Position }, range: { start?: number; length?: number }): Range {
|
||||
return Range.create(document.positionAt(range.start || 0), document.positionAt((range.start || 0) + (range.length || 0)));
|
||||
}
|
||||
|
||||
export function convertToLocationRange(defDoc: SnapshotFragment, textSpan: ts.TextSpan): Range {
|
||||
const range = mapRangeToOriginal(defDoc, convertRange(defDoc, textSpan));
|
||||
// Some definition like the svelte component class definition don't exist in the original, so we map to 0,1
|
||||
if (range.start.line < 0) {
|
||||
range.start.line = 0;
|
||||
range.start.character = 1;
|
||||
}
|
||||
if (range.end.line < 0) {
|
||||
range.end = range.start;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
type FrameworkExt = 'astro' | 'vue' | 'jsx' | 'tsx' | 'svelte';
|
||||
|
||||
export function isVirtualFrameworkFilePath(ext: FrameworkExt, filePath: string) {
|
||||
return filePath.endsWith('.' + ext + '.ts');
|
||||
}
|
||||
|
||||
export function isAstroFilePath(filePath: string) {
|
||||
return filePath.endsWith('.astro');
|
||||
}
|
||||
|
||||
export function isVirtualAstroFilePath(filePath: string) {
|
||||
return filePath.endsWith('.astro.ts');
|
||||
return isVirtualFrameworkFilePath('astro', filePath);
|
||||
}
|
||||
|
||||
export function isVirtualVueFilePath(filePath: string) {
|
||||
return isVirtualFrameworkFilePath('vue', filePath);
|
||||
}
|
||||
|
||||
export function isVirtualJsxFilePath(filePath: string) {
|
||||
return isVirtualFrameworkFilePath('jsx', filePath) || isVirtualFrameworkFilePath('tsx', filePath);
|
||||
}
|
||||
|
||||
export function isVirtualSvelteFilePath(filePath: string) {
|
||||
return isVirtualFrameworkFilePath('svelte', filePath);
|
||||
}
|
||||
|
||||
export function isVirtualFilePath(filePath: string) {
|
||||
return isVirtualAstroFilePath(filePath) || isVirtualVueFilePath(filePath) || isVirtualSvelteFilePath(filePath) || isVirtualJsxFilePath(filePath);
|
||||
}
|
||||
|
||||
export function toVirtualAstroFilePath(filePath: string) {
|
||||
|
|
|
@ -51,6 +51,10 @@ export function clamp(num: number, min: number, max: number): number {
|
|||
return Math.max(min, Math.min(max, num));
|
||||
}
|
||||
|
||||
export function isNotNullOrUndefined<T>(val: T | undefined | null): val is T {
|
||||
return val !== undefined && val !== null;
|
||||
}
|
||||
|
||||
/** Checks if a position is inside range */
|
||||
export function isInRange(positionToTest: Position, range: Range): boolean {
|
||||
return isBeforeOrEqualToPosition(range.end, positionToTest) && isBeforeOrEqualToPosition(positionToTest, range.start);
|
||||
|
|
Loading…
Reference in a new issue