summaryrefslogtreecommitdiff
path: root/tools/astro-languageserver/src
diff options
context:
space:
mode:
authorGravatar Matthew Phillips <matthew@matthewphillips.info> 2021-05-20 13:02:46 -0400
committerGravatar GitHub <noreply@github.com> 2021-05-20 13:02:46 -0400
commit6ce068b838a165c65898b5581f7508b75e256efc (patch)
tree998e1f1cbaf5eadcd9b88a91fd68700ba89bb4a8 /tools/astro-languageserver/src
parent3da2b58b9d1392739e136d406e4148f846f0cec1 (diff)
downloadastro-6ce068b838a165c65898b5581f7508b75e256efc.tar.gz
astro-6ce068b838a165c65898b5581f7508b75e256efc.tar.zst
astro-6ce068b838a165c65898b5581f7508b75e256efc.zip
Get definitions (#219)
* Start on css completion * Support for CSS completions * Adds support for Go to Definition in TypeScript in Astro * Run formatting
Diffstat (limited to 'tools/astro-languageserver/src')
-rw-r--r--tools/astro-languageserver/src/core/documents/utils.ts7
-rw-r--r--tools/astro-languageserver/src/index.ts6
-rw-r--r--tools/astro-languageserver/src/plugins/PluginHost.ts30
-rw-r--r--tools/astro-languageserver/src/plugins/typescript/DocumentSnapshot.ts242
-rw-r--r--tools/astro-languageserver/src/plugins/typescript/LanguageServiceManager.ts3
-rw-r--r--tools/astro-languageserver/src/plugins/typescript/SnapshotManager.ts214
-rw-r--r--tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts40
-rw-r--r--tools/astro-languageserver/src/plugins/typescript/features/utils.ts54
-rw-r--r--tools/astro-languageserver/src/plugins/typescript/languageService.ts3
-rw-r--r--tools/astro-languageserver/src/plugins/typescript/utils.ts46
-rw-r--r--tools/astro-languageserver/src/utils.ts4
11 files changed, 430 insertions, 219 deletions
diff --git a/tools/astro-languageserver/src/core/documents/utils.ts b/tools/astro-languageserver/src/core/documents/utils.ts
index 227a76176..eb9d2060d 100644
--- a/tools/astro-languageserver/src/core/documents/utils.ts
+++ b/tools/astro-languageserver/src/core/documents/utils.ts
@@ -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
diff --git a/tools/astro-languageserver/src/index.ts b/tools/astro-languageserver/src/index.ts
index 7c4417d69..e3532f252 100644
--- a/tools/astro-languageserver/src/index.ts
+++ b/tools/astro-languageserver/src/index.ts
@@ -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));
diff --git a/tools/astro-languageserver/src/plugins/PluginHost.ts b/tools/astro-languageserver/src/plugins/PluginHost.ts
index 8d59c9c93..3741845c4 100644
--- a/tools/astro-languageserver/src/plugins/PluginHost.ts
+++ b/tools/astro-languageserver/src/plugins/PluginHost.ts
@@ -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);
diff --git a/tools/astro-languageserver/src/plugins/typescript/DocumentSnapshot.ts b/tools/astro-languageserver/src/plugins/typescript/DocumentSnapshot.ts
new file mode 100644
index 000000000..04ea170d4
--- /dev/null
+++ b/tools/astro-languageserver/src/plugins/typescript/DocumentSnapshot.ts
@@ -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++;
+ }
+}
diff --git a/tools/astro-languageserver/src/plugins/typescript/LanguageServiceManager.ts b/tools/astro-languageserver/src/plugins/typescript/LanguageServiceManager.ts
index 529ab2b4c..d2a5cecff 100644
--- a/tools/astro-languageserver/src/plugins/typescript/LanguageServiceManager.ts
+++ b/tools/astro-languageserver/src/plugins/typescript/LanguageServiceManager.ts
@@ -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;
diff --git a/tools/astro-languageserver/src/plugins/typescript/SnapshotManager.ts b/tools/astro-languageserver/src/plugins/typescript/SnapshotManager.ts
index 4f9e865a1..5a406b945 100644
--- a/tools/astro-languageserver/src/plugins/typescript/SnapshotManager.ts
+++ b/tools/astro-languageserver/src/plugins/typescript/SnapshotManager.ts
@@ -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++;
- }
-}
diff --git a/tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts b/tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts
index aab758bdb..30781a508 100644
--- a/tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts
+++ b/tools/astro-languageserver/src/plugins/typescript/TypeScriptPlugin.ts
@@ -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>();
diff --git a/tools/astro-languageserver/src/plugins/typescript/features/utils.ts b/tools/astro-languageserver/src/plugins/typescript/features/utils.ts
new file mode 100644
index 000000000..8c87dc5f4
--- /dev/null
+++ b/tools/astro-languageserver/src/plugins/typescript/features/utils.ts
@@ -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;
+ }
+}
diff --git a/tools/astro-languageserver/src/plugins/typescript/languageService.ts b/tools/astro-languageserver/src/plugins/typescript/languageService.ts
index 0db9e66cc..b7ff6df20 100644
--- a/tools/astro-languageserver/src/plugins/typescript/languageService.ts
+++ b/tools/astro-languageserver/src/plugins/typescript/languageService.ts
@@ -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>>();
diff --git a/tools/astro-languageserver/src/plugins/typescript/utils.ts b/tools/astro-languageserver/src/plugins/typescript/utils.ts
index da4e37c84..b212d9cd3 100644
--- a/tools/astro-languageserver/src/plugins/typescript/utils.ts
+++ b/tools/astro-languageserver/src/plugins/typescript/utils.ts
@@ -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) {
diff --git a/tools/astro-languageserver/src/utils.ts b/tools/astro-languageserver/src/utils.ts
index f9f1acf34..ba3d9366e 100644
--- a/tools/astro-languageserver/src/utils.ts
+++ b/tools/astro-languageserver/src/utils.ts
@@ -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);