VS Code extension (#197)
* Fix running the extension I'm not sure how my setup was different but I was unable to get the extension to run locally without adding a binary. This mirrors what Svelte does so I'm assuming it's the way it's supposed to be loaded. * Resolve TypeScript suggestions to the correct file This fixes a couple of bugs related to suggestions. 1 was this does the whole `.ts` extension fakeout thing so that the TypeScript plugin thinks that Astro files are TypeScript. Secondly this fixes the caching of the Document, so that suggestions account for the current document text.
This commit is contained in:
parent
e77c8fff77
commit
88529b679a
7 changed files with 182 additions and 20 deletions
|
@ -8,7 +8,7 @@ import glob from 'tiny-glob';
|
|||
/** @type {import('esbuild').BuildOptions} */
|
||||
const defaultConfig = {
|
||||
bundle: true,
|
||||
minify: true,
|
||||
minify: false,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
target: 'node14',
|
||||
|
|
|
@ -29,7 +29,7 @@ export function startServer() {
|
|||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
foldingRangeProvider: true,
|
||||
completionProvider: {
|
||||
resolveProvider: false,
|
||||
resolveProvider: true,
|
||||
triggerCharacters: [
|
||||
'.',
|
||||
'"',
|
||||
|
@ -70,7 +70,9 @@ export function startServer() {
|
|||
|
||||
connection.onDidCloseTextDocument((evt) => docManager.closeDocument(evt.textDocument.uri));
|
||||
|
||||
connection.onDidChangeTextDocument((evt) => docManager.updateDocument(evt.textDocument.uri, evt.contentChanges));
|
||||
connection.onDidChangeTextDocument((evt) => {
|
||||
docManager.updateDocument(evt.textDocument.uri, evt.contentChanges)
|
||||
});
|
||||
|
||||
connection.onDidChangeWatchedFiles((evt) => {
|
||||
const params = evt.changes
|
||||
|
|
|
@ -37,7 +37,7 @@ export class SnapshotManager {
|
|||
}
|
||||
previousSnapshot.update(changes);
|
||||
} else {
|
||||
const newSnapshot = createDocumentSnapshot(fileName);
|
||||
const newSnapshot = createDocumentSnapshot(fileName, null);
|
||||
|
||||
if (previousSnapshot) {
|
||||
newSnapshot.version = previousSnapshot.version + 1;
|
||||
|
@ -120,8 +120,8 @@ export interface DocumentSnapshot extends ts.IScriptSnapshot {
|
|||
getFullText(): string;
|
||||
}
|
||||
|
||||
export const createDocumentSnapshot = (filePath: string, createDocument?: (_filePath: string, text: string) => Document): DocumentSnapshot => {
|
||||
const text = ts.sys.readFile(filePath) ?? '';
|
||||
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');
|
||||
|
|
|
@ -25,7 +25,13 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
|
|||
const { tsDoc, lang } = await this.lang.getTypeScriptDoc(document);
|
||||
const fragment = await tsDoc.getFragment();
|
||||
|
||||
const { entries } = lang.getCompletionsAtPosition(fragment.filePath, document.offsetAt(position), {}) ?? { entries: [] };
|
||||
const offset = document.offsetAt(position);
|
||||
const entries = lang.getCompletionsAtPosition(fragment.filePath, offset, {
|
||||
importModuleSpecifierPreference: 'relative',
|
||||
importModuleSpecifierEnding: 'auto',
|
||||
quotePreference: 'single'
|
||||
})?.entries || [];
|
||||
|
||||
const completionItems = entries
|
||||
.map((entry: ts.CompletionEntry) => this.toCompletionItem(fragment, entry, document.uri, position, new Set()))
|
||||
.filter((i) => i) as CompletionItem[];
|
||||
|
@ -37,12 +43,16 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
|
|||
const { data: comp } = completionItem;
|
||||
const { tsDoc, lang } = await this.lang.getTypeScriptDoc(document);
|
||||
|
||||
const filePath = tsDoc.filePath;
|
||||
let filePath = tsDoc.filePath;
|
||||
|
||||
if (!comp || !filePath) {
|
||||
return completionItem;
|
||||
}
|
||||
|
||||
if(filePath.endsWith('.astro')) {
|
||||
filePath = filePath + '.ts';
|
||||
}
|
||||
|
||||
const fragment = await tsDoc.getFragment();
|
||||
const detail = lang.getCompletionEntryDetails(filePath, fragment.offsetAt(comp.position), comp.name, {}, comp.source, {});
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
import * as ts from 'typescript';
|
||||
import { basename } from 'path';
|
||||
import { ensureRealAstroFilePath, findTsConfigPath, isAstroFilePath, toVirtualAstroFilePath } from './utils';
|
||||
import { ensureRealAstroFilePath, findTsConfigPath } from './utils';
|
||||
import { Document } from '../../core/documents';
|
||||
import { createDocumentSnapshot, SnapshotManager, DocumentSnapshot } from './SnapshotManager';
|
||||
import { createAstroSys } from './astro-sys';
|
||||
import { createAstroModuleLoader } from './module-loader';
|
||||
|
||||
const services = new Map<string, Promise<LanguageServiceContainer>>();
|
||||
|
||||
|
@ -72,18 +72,19 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
|
|||
|
||||
let projectVersion = 0;
|
||||
const snapshotManager = new SnapshotManager(project.fileNames, { exclude: ['node_modules', 'dist'], include: ['astro'] }, workspaceRoot || process.cwd());
|
||||
const astroSys = createAstroSys(updateDocument);
|
||||
|
||||
const astroModuleLoader = createAstroModuleLoader(getScriptSnapshot, {});
|
||||
|
||||
const host: ts.LanguageServiceHost = {
|
||||
getNewLine: () => ts.sys.newLine,
|
||||
useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
|
||||
readFile: astroSys.readFile,
|
||||
writeFile: astroSys.writeFile,
|
||||
fileExists: astroSys.fileExists,
|
||||
directoryExists: astroSys.directoryExists,
|
||||
getDirectories: astroSys.getDirectories,
|
||||
readDirectory: astroSys.readDirectory,
|
||||
realpath: astroSys.realpath,
|
||||
readFile: astroModuleLoader.readFile,
|
||||
writeFile: astroModuleLoader.writeFile,
|
||||
fileExists: astroModuleLoader.fileExists,
|
||||
directoryExists: astroModuleLoader.directoryExists,
|
||||
getDirectories: astroModuleLoader.getDirectories,
|
||||
readDirectory: astroModuleLoader.readDirectory,
|
||||
realpath: astroModuleLoader.realpath,
|
||||
|
||||
getCompilationSettings: () => project.options,
|
||||
getCurrentDirectory: () => workspaceRoot,
|
||||
|
@ -127,7 +128,8 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
|
|||
return previousSnapshot;
|
||||
}
|
||||
|
||||
const snapshot = createDocumentSnapshot(filePath, docContext.createDocument);
|
||||
const currentText = document ? document.getText() : null;
|
||||
const snapshot = createDocumentSnapshot(filePath, currentText, docContext.createDocument);
|
||||
snapshotManager.set(filePath, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
@ -140,7 +142,7 @@ async function createLanguageService(tsconfigPath: string, workspaceRoot: string
|
|||
return doc;
|
||||
}
|
||||
|
||||
doc = createDocumentSnapshot(fileName, docContext.createDocument);
|
||||
doc = createDocumentSnapshot(fileName, null, docContext.createDocument);
|
||||
snapshotManager.set(fileName, doc);
|
||||
return doc;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
import ts from 'typescript';
|
||||
import type { DocumentSnapshot } from './SnapshotManager';
|
||||
import {
|
||||
isVirtualAstroFilePath,
|
||||
ensureRealAstroFilePath,
|
||||
getExtensionFromScriptKind
|
||||
} from './utils';
|
||||
import { createAstroSys } from './astro-sys';
|
||||
|
||||
/**
|
||||
* Caches resolved modules.
|
||||
*/
|
||||
class ModuleResolutionCache {
|
||||
private cache = new Map<string, ts.ResolvedModule>();
|
||||
|
||||
/**
|
||||
* Tries to get a cached module.
|
||||
*/
|
||||
get(moduleName: string, containingFile: string): ts.ResolvedModule | undefined {
|
||||
return this.cache.get(this.getKey(moduleName, containingFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches resolved module, if it is not undefined.
|
||||
*/
|
||||
set(moduleName: string, containingFile: string, resolvedModule: ts.ResolvedModule | undefined) {
|
||||
if (!resolvedModule) {
|
||||
return;
|
||||
}
|
||||
this.cache.set(this.getKey(moduleName, containingFile), resolvedModule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes module from cache. Call this if a file was deleted.
|
||||
* @param resolvedModuleName full path of the module
|
||||
*/
|
||||
delete(resolvedModuleName: string): void {
|
||||
this.cache.forEach((val, key) => {
|
||||
if (val.resolvedFileName === resolvedModuleName) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getKey(moduleName: string, containingFile: string) {
|
||||
return containingFile + ':::' + ensureRealAstroFilePath(moduleName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a module loader specifically for `.astro` files.
|
||||
*
|
||||
* The typescript language service tries to look up other files that are referenced in the currently open astro file.
|
||||
* For `.ts`/`.js` files this works, for `.astro` files it does not by default.
|
||||
* Reason: The typescript language service does not know about the `.astro` file ending,
|
||||
* so it assumes it's a normal typescript file and searches for files like `../Component.astro.ts`, which is wrong.
|
||||
* In order to fix this, we need to wrap typescript's module resolution and reroute all `.astro.ts` file lookups to .astro.
|
||||
*
|
||||
* @param getSnapshot A function which returns a (in case of astro file fully preprocessed) typescript/javascript snapshot
|
||||
* @param compilerOptions The typescript compiler options
|
||||
*/
|
||||
export function createAstroModuleLoader(
|
||||
getSnapshot: (fileName: string) => DocumentSnapshot,
|
||||
compilerOptions: ts.CompilerOptions
|
||||
) {
|
||||
const astroSys = createAstroSys(getSnapshot);
|
||||
const moduleCache = new ModuleResolutionCache();
|
||||
|
||||
return {
|
||||
fileExists: astroSys.fileExists,
|
||||
readFile: astroSys.readFile,
|
||||
writeFile: astroSys.writeFile,
|
||||
readDirectory: astroSys.readDirectory,
|
||||
directoryExists: astroSys.directoryExists,
|
||||
getDirectories: astroSys.getDirectories,
|
||||
realpath: astroSys.realpath,
|
||||
deleteFromModuleCache: (path: string) => moduleCache.delete(path),
|
||||
resolveModuleNames
|
||||
};
|
||||
|
||||
function resolveModuleNames(
|
||||
moduleNames: string[],
|
||||
containingFile: string
|
||||
): Array<ts.ResolvedModule | undefined> {
|
||||
return moduleNames.map((moduleName) => {
|
||||
const cachedModule = moduleCache.get(moduleName, containingFile);
|
||||
if (cachedModule) {
|
||||
return cachedModule;
|
||||
}
|
||||
|
||||
const resolvedModule = resolveModuleName(moduleName, containingFile);
|
||||
moduleCache.set(moduleName, containingFile, resolvedModule);
|
||||
return resolvedModule;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveModuleName(
|
||||
name: string,
|
||||
containingFile: string
|
||||
): ts.ResolvedModule | undefined {
|
||||
// Delegate to the TS resolver first.
|
||||
// If that does not bring up anything, try the Astro Module loader
|
||||
// which is able to deal with .astro files.
|
||||
const tsResolvedModule = ts.resolveModuleName(name, containingFile, compilerOptions, ts.sys)
|
||||
.resolvedModule;
|
||||
if (tsResolvedModule && !isVirtualAstroFilePath(tsResolvedModule.resolvedFileName)) {
|
||||
return tsResolvedModule;
|
||||
}
|
||||
|
||||
const astroResolvedModule = ts.resolveModuleName(
|
||||
name,
|
||||
containingFile,
|
||||
compilerOptions,
|
||||
astroSys
|
||||
).resolvedModule;
|
||||
if (
|
||||
!astroResolvedModule ||
|
||||
!isVirtualAstroFilePath(astroResolvedModule.resolvedFileName)
|
||||
) {
|
||||
return astroResolvedModule;
|
||||
}
|
||||
|
||||
const resolvedFileName = ensureRealAstroFilePath(astroResolvedModule.resolvedFileName);
|
||||
const snapshot = getSnapshot(resolvedFileName);
|
||||
|
||||
const resolvedastroModule: ts.ResolvedModuleFull = {
|
||||
extension: getExtensionFromScriptKind(snapshot && snapshot.scriptKind),
|
||||
resolvedFileName
|
||||
};
|
||||
return resolvedastroModule;
|
||||
}
|
||||
}
|
|
@ -111,6 +111,22 @@ export function getScriptKindFromFileName(fileName: string): ts.ScriptKind {
|
|||
}
|
||||
}
|
||||
|
||||
export function getExtensionFromScriptKind(kind: ts.ScriptKind | undefined): ts.Extension {
|
||||
switch (kind) {
|
||||
case ts.ScriptKind.JSX:
|
||||
return ts.Extension.Jsx;
|
||||
case ts.ScriptKind.TS:
|
||||
return ts.Extension.Ts;
|
||||
case ts.ScriptKind.TSX:
|
||||
return ts.Extension.Tsx;
|
||||
case ts.ScriptKind.JSON:
|
||||
return ts.Extension.Json;
|
||||
case ts.ScriptKind.JS:
|
||||
default:
|
||||
return ts.Extension.Js;
|
||||
}
|
||||
}
|
||||
|
||||
export function isAstroFilePath(filePath: string) {
|
||||
return filePath.endsWith('.astro');
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue