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 { 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';
|
import { parseHtml } from './parseHtml';
|
||||||
|
|
||||||
export interface TagInformation {
|
export interface TagInformation {
|
||||||
|
@ -84,6 +85,10 @@ export function isInsideFrontmatter(text: string, offset: number): boolean {
|
||||||
return start > 1 && start < 3 && end >= 1;
|
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
|
* Get the line and character based on the offset
|
||||||
* @param offset The index of the position
|
* @param offset The index of the position
|
||||||
|
|
|
@ -19,6 +19,10 @@ export function startServer() {
|
||||||
connection.onInitialize((evt) => {
|
connection.onInitialize((evt) => {
|
||||||
const workspaceUris = evt.workspaceFolders?.map((folder) => folder.uri.toString()) ?? [evt.rootUri ?? ''];
|
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 AstroPlugin(docManager, configManager));
|
||||||
pluginHost.register(new HTMLPlugin(docManager, configManager));
|
pluginHost.register(new HTMLPlugin(docManager, configManager));
|
||||||
pluginHost.register(new CSSPlugin(docManager, configManager));
|
pluginHost.register(new CSSPlugin(docManager, configManager));
|
||||||
|
@ -29,6 +33,7 @@ export function startServer() {
|
||||||
capabilities: {
|
capabilities: {
|
||||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||||
foldingRangeProvider: true,
|
foldingRangeProvider: true,
|
||||||
|
definitionProvider: true,
|
||||||
completionProvider: {
|
completionProvider: {
|
||||||
resolveProvider: true,
|
resolveProvider: true,
|
||||||
triggerCharacters: [
|
triggerCharacters: [
|
||||||
|
@ -102,6 +107,7 @@ export function startServer() {
|
||||||
|
|
||||||
return pluginHost.resolveCompletion(data, completionItem);
|
return pluginHost.resolveCompletion(data, completionItem);
|
||||||
});
|
});
|
||||||
|
connection.onDefinition((evt) => pluginHost.getDefinitions(evt.textDocument, evt.position));
|
||||||
connection.onFoldingRanges((evt) => pluginHost.getFoldingRanges(evt.textDocument));
|
connection.onFoldingRanges((evt) => pluginHost.getFoldingRanges(evt.textDocument));
|
||||||
connection.onRequest(TagCloseRequest, (evt: any) => pluginHost.doTagComplete(evt.textDocument, evt.position));
|
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 { DocumentManager } from '../core/documents';
|
||||||
import type * as d from './interfaces';
|
import type * as d from './interfaces';
|
||||||
import { flatten } from '../utils';
|
import { flatten } from '../utils';
|
||||||
|
@ -10,11 +10,24 @@ enum ExecuteMode {
|
||||||
Collect,
|
Collect,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PluginHostConfig {
|
||||||
|
filterIncompleteCompletions: boolean;
|
||||||
|
definitionLinkSupport: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class PluginHost {
|
export class PluginHost {
|
||||||
private plugins: d.Plugin[] = [];
|
private plugins: d.Plugin[] = [];
|
||||||
|
private pluginHostConfig: PluginHostConfig = {
|
||||||
|
filterIncompleteCompletions: true,
|
||||||
|
definitionLinkSupport: false,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private documentsManager: DocumentManager) {}
|
constructor(private documentsManager: DocumentManager) {}
|
||||||
|
|
||||||
|
initialize(pluginHostConfig: PluginHostConfig) {
|
||||||
|
this.pluginHostConfig = pluginHostConfig;
|
||||||
|
}
|
||||||
|
|
||||||
register(plugin: d.Plugin) {
|
register(plugin: d.Plugin) {
|
||||||
this.plugins.push(plugin);
|
this.plugins.push(plugin);
|
||||||
}
|
}
|
||||||
|
@ -67,6 +80,21 @@ export class PluginHost {
|
||||||
return foldingRanges;
|
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 {
|
onWatchFileChanges(onWatchFileChangesParams: any[]): void {
|
||||||
for (const support of this.plugins) {
|
for (const support of this.plugins) {
|
||||||
support.onWatchFileChanges?.(onWatchFileChangesParams);
|
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 type { ConfigManager } from '../../core/config';
|
||||||
import { urlToPath, pathToUrl, debounceSameArg } from '../../utils';
|
import { urlToPath, pathToUrl, debounceSameArg } from '../../utils';
|
||||||
import { getLanguageService, getLanguageServiceForDocument, LanguageServiceContainer, LanguageServiceDocumentContext } from './languageService';
|
import { getLanguageService, getLanguageServiceForDocument, LanguageServiceContainer, LanguageServiceDocumentContext } from './languageService';
|
||||||
import { DocumentSnapshot, SnapshotManager } from './SnapshotManager';
|
import { SnapshotManager } from './SnapshotManager';
|
||||||
|
import { DocumentSnapshot } from './DocumentSnapshot';
|
||||||
|
|
||||||
export class LanguageServiceManager {
|
export class LanguageServiceManager {
|
||||||
private readonly docManager: DocumentManager;
|
private readonly docManager: DocumentManager;
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import { TextDocumentContentChangeEvent, Position } from 'vscode-languageserver';
|
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
|
||||||
import { Document } from '../../core/documents';
|
import { toVirtualAstroFilePath } from './utils';
|
||||||
import { positionAt, offsetAt } from '../../core/documents/utils';
|
import { DocumentSnapshot, TypeScriptDocumentSnapshot, createDocumentSnapshot } from './DocumentSnapshot';
|
||||||
import { pathToUrl } from '../../utils';
|
|
||||||
import { getScriptKindFromFileName, isAstroFilePath, toVirtualAstroFilePath } from './utils';
|
|
||||||
|
|
||||||
export interface TsFilesSpec {
|
export interface TsFilesSpec {
|
||||||
include?: readonly string[];
|
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 { Document, DocumentManager } 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 { CompletionContext, Position, FileChangeType } 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 { 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 {
|
export class TypeScriptPlugin implements CompletionsProvider {
|
||||||
private readonly docManager: DocumentManager;
|
private readonly docManager: DocumentManager;
|
||||||
|
@ -33,6 +35,40 @@ export class TypeScriptPlugin implements CompletionsProvider {
|
||||||
return this.completionProvider.resolveCompletion(document, completionItem);
|
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> {
|
async onWatchFileChanges(onWatchFileChangesParams: any[]): Promise<void> {
|
||||||
const doneUpdateProjectFiles = new Set<SnapshotManager>();
|
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 { basename } from 'path';
|
||||||
import { ensureRealAstroFilePath, findTsConfigPath } from './utils';
|
import { ensureRealAstroFilePath, findTsConfigPath } from './utils';
|
||||||
import { Document } from '../../core/documents';
|
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';
|
import { createAstroModuleLoader } from './module-loader';
|
||||||
|
|
||||||
const services = new Map<string, Promise<LanguageServiceContainer>>();
|
const services = new Map<string, Promise<LanguageServiceContainer>>();
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
import { CompletionItemKind, DiagnosticSeverity } from 'vscode-languageserver';
|
import { CompletionItemKind, DiagnosticSeverity, Position, Range } from 'vscode-languageserver';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { pathToUrl } from '../../utils';
|
import { pathToUrl } from '../../utils';
|
||||||
|
import { mapRangeToOriginal } from '../../core/documents';
|
||||||
|
import { SnapshotFragment } from './DocumentSnapshot';
|
||||||
|
|
||||||
export function scriptElementKindToCompletionItemKind(kind: ts.ScriptElementKind): CompletionItemKind {
|
export function scriptElementKindToCompletionItemKind(kind: ts.ScriptElementKind): CompletionItemKind {
|
||||||
switch (kind) {
|
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) {
|
export function isAstroFilePath(filePath: string) {
|
||||||
return filePath.endsWith('.astro');
|
return filePath.endsWith('.astro');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVirtualAstroFilePath(filePath: string) {
|
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) {
|
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));
|
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 */
|
/** Checks if a position is inside range */
|
||||||
export function isInRange(positionToTest: Position, range: Range): boolean {
|
export function isInRange(positionToTest: Position, range: Range): boolean {
|
||||||
return isBeforeOrEqualToPosition(range.end, positionToTest) && isBeforeOrEqualToPosition(positionToTest, range.start);
|
return isBeforeOrEqualToPosition(range.end, positionToTest) && isBeforeOrEqualToPosition(positionToTest, range.start);
|
||||||
|
|
Loading…
Add table
Reference in a new issue