Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(kit): add support for checking project references files #232

Merged
merged 1 commit into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 124 additions & 89 deletions packages/kit/lib/createChecker.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,30 @@
import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, LanguagePlugin, NotificationHandler, LanguageServicePlugin, LanguageServiceEnvironment, createLanguageService, mergeWorkspaceEdits, createLanguage, createUriMap } from '@volar/language-service';
import { CodeActionTriggerKind, Diagnostic, DiagnosticSeverity, DidChangeWatchedFilesParams, FileChangeType, Language, LanguagePlugin, LanguageServiceEnvironment, LanguageServicePlugin, NotificationHandler, createLanguage, createLanguageService, createUriMap, mergeWorkspaceEdits } from '@volar/language-service';
import { TypeScriptProjectHost, createLanguageServiceHost, resolveFileLanguageId } from '@volar/typescript';
import * as path from 'typesafe-path/posix';
import * as ts from 'typescript';
import { TextDocument } from 'vscode-languageserver-textdocument';
import { createServiceEnvironment } from './createServiceEnvironment';
import { asPosix, defaultCompilerOptions, asUri, asFileName } from './utils';
import { URI } from 'vscode-uri';
import { TypeScriptProjectHost, createLanguageServiceHost, resolveFileLanguageId } from '@volar/typescript';
import { createServiceEnvironment } from './createServiceEnvironment';
import { asFileName, asPosix, asUri, defaultCompilerOptions } from './utils';

export function createTypeScriptChecker(
languagePlugins: LanguagePlugin<URI>[],
languageServicePlugins: LanguageServicePlugin[],
tsconfig: string
tsconfig: string,
includeProjectReference = false
) {
const tsconfigPath = asPosix(tsconfig);
return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, tsconfigPath, env => {
return createTypeScriptProjectHost(
env,
() => {
const parsed = ts.parseJsonSourceFileConfigFileContent(
ts.readJsonConfigFile(tsconfigPath, ts.sys.readFile),
ts.sys,
path.dirname(tsconfigPath),
undefined,
tsconfigPath,
undefined,
languagePlugins.map(plugin => plugin.typescript?.extraFileExtensions ?? []).flat()
);
parsed.fileNames = parsed.fileNames.map(asPosix);
return parsed;
}
return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, tsconfigPath, () => {
return ts.parseJsonSourceFileConfigFileContent(
ts.readJsonConfigFile(tsconfigPath, ts.sys.readFile),
ts.sys,
path.dirname(tsconfigPath),
undefined,
tsconfigPath,
undefined,
languagePlugins.map(plugin => plugin.typescript?.extraFileExtensions ?? []).flat()
);
});
}, includeProjectReference);
}

export function createTypeScriptInferredChecker(
Expand All @@ -39,15 +33,13 @@ export function createTypeScriptInferredChecker(
getScriptFileNames: () => string[],
compilerOptions = defaultCompilerOptions
) {
return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, undefined, env => {
return createTypeScriptProjectHost(
env,
() => ({
options: compilerOptions,
fileNames: getScriptFileNames().map(asPosix),
})
);
});
return createTypeScriptCheckerWorker(languagePlugins, languageServicePlugins, undefined, () => {
return {
options: compilerOptions,
fileNames: getScriptFileNames(),
errors: [],
};
}, false);
}

const fsFileSnapshots = createUriMap<[number | undefined, ts.IScriptSnapshot | undefined]>();
Expand All @@ -56,14 +48,13 @@ function createTypeScriptCheckerWorker(
languagePlugins: LanguagePlugin<URI>[],
languageServicePlugins: LanguageServicePlugin[],
configFileName: string | undefined,
getProjectHost: (env: LanguageServiceEnvironment) => TypeScriptProjectHost
getCommandLine: () => ts.ParsedCommandLine,
includeProjectReference: boolean
) {

let settings = {};

const env = createServiceEnvironment(() => settings);
const didChangeWatchedFilesCallbacks = new Set<NotificationHandler<DidChangeWatchedFilesParams>>();

const env = createServiceEnvironment(() => settings);
env.onDidChangeWatchedFiles = cb => {
didChangeWatchedFilesCallbacks.add(cb);
return {
Expand All @@ -72,15 +63,16 @@ function createTypeScriptCheckerWorker(
},
};
};

const language = createLanguage(
[
...languagePlugins,
{ getLanguageId: uri => resolveFileLanguageId(uri.path) },
],
createUriMap(ts.sys.useCaseSensitiveFileNames),
uri => {
// fs files
(uri, includeFsFiles) => {
if (!includeFsFiles) {
return;
}
const cache = fsFileSnapshots.get(uri);
const fileName = asFileName(uri);
const modifiedTime = ts.sys.getModifiedTime?.(fileName)?.valueOf();
Expand All @@ -103,36 +95,40 @@ function createTypeScriptCheckerWorker(
}
}
);
const projectHost = getProjectHost(env);
const languageService = createLanguageService(
language,
languageServicePlugins,
env,
{
typescript: {
configFileName,
sys: ts.sys,
uriConverter: {
asFileName,
asUri,
},
...createLanguageServiceHost(
ts,
ts.sys,
language,
asUri,
projectHost
),
},
const [projectHost, languageService] = createTypeScriptCheckerLanguageService(env, language, languageServicePlugins, configFileName, getCommandLine);
const projectReferenceLanguageServices = new Map<string, ReturnType<typeof createTypeScriptCheckerLanguageService>>();

if (includeProjectReference) {
const tsconfigs = new Set<string>();
const tsLs: ts.LanguageService = languageService.context.inject('typescript/languageService');
const projectReferences = tsLs.getProgram()?.getResolvedProjectReferences();
if (configFileName) {
tsconfigs.add(asPosix(configFileName));
}
);
projectReferences?.forEach(visit);

function visit(ref: ts.ResolvedProjectReference | undefined) {
if (ref && !tsconfigs.has(ref.sourceFile.fileName)) {
tsconfigs.add(ref.sourceFile.fileName);
const projectReferenceLanguageService = createTypeScriptCheckerLanguageService(env, language, languageServicePlugins, ref.sourceFile.fileName, () => ref.commandLine);
projectReferenceLanguageServices.set(ref.sourceFile.fileName, projectReferenceLanguageService);
ref.references?.forEach(visit);
}
}
}

return {
// apis
check,
fixErrors,
printErrors,
projectHost,
getRootFileNames: () => {
const fileNames = projectHost.getScriptFileNames();
for (const [projectHost] of projectReferenceLanguageServices.values()) {
fileNames.push(...projectHost.getScriptFileNames());
}
return [...new Set(fileNames)];
},
language,

// settings
Expand Down Expand Up @@ -165,12 +161,14 @@ function createTypeScriptCheckerWorker(
function check(fileName: string) {
fileName = asPosix(fileName);
const uri = asUri(fileName);
const languageService = getLanguageServiceForFile(fileName);
return languageService.getDiagnostics(uri);
}

async function fixErrors(fileName: string, diagnostics: Diagnostic[], only: string[] | undefined, writeFile: (fileName: string, newText: string) => Promise<void>) {
fileName = asPosix(fileName);
const uri = asUri(fileName);
const languageService = getLanguageServiceForFile(fileName);
const sourceScript = languageService.context.language.scripts.get(uri);
if (sourceScript) {
const document = languageService.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot);
Expand Down Expand Up @@ -224,6 +222,7 @@ function createTypeScriptCheckerWorker(
function formatErrors(fileName: string, diagnostics: Diagnostic[], rootPath: string) {
fileName = asPosix(fileName);
const uri = asUri(fileName);
const languageService = getLanguageServiceForFile(fileName);
const sourceScript = languageService.context.language.scripts.get(uri)!;
const document = languageService.context.documents.get(uri, sourceScript.languageId, sourceScript.snapshot);
const errors: ts.Diagnostic[] = diagnostics.map<ts.Diagnostic>(diagnostic => ({
Expand All @@ -241,81 +240,117 @@ function createTypeScriptCheckerWorker(
});
return text;
}

function getLanguageServiceForFile(fileName: string) {
if (!includeProjectReference) {
return languageService;
}
fileName = asPosix(fileName);
for (const [_1, languageService] of projectReferenceLanguageServices.values()) {
const tsLs: ts.LanguageService = languageService.context.inject('typescript/languageService');
if (tsLs.getProgram()?.getSourceFile(fileName)) {
return languageService;
}
}
return languageService;
}
}

function createTypeScriptProjectHost(
function createTypeScriptCheckerLanguageService(
env: LanguageServiceEnvironment,
createParsedCommandLine: () => Pick<ts.ParsedCommandLine, 'options' | 'fileNames'>
language: Language<URI>,
languageServicePlugins: LanguageServicePlugin[],
configFileName: string | undefined,
getCommandLine: () => ts.ParsedCommandLine
) {
let scriptSnapshotsCache: Map<string, ts.IScriptSnapshot | undefined> = new Map();
let parsedCommandLine = createParsedCommandLine();
let commandLine = getCommandLine();
let projectVersion = 0;
let shouldCheckRootFiles = false;

const host: TypeScriptProjectHost = {
const resolvedFileNameByCommandLine = new WeakMap<ts.ParsedCommandLine, string[]>();
const projectHost: TypeScriptProjectHost = {
getCurrentDirectory: () => env.workspaceFolders.length
? asFileName(env.workspaceFolders[0])
: process.cwd(),
getCompilationSettings: () => {
return parsedCommandLine.options;
return commandLine.options;
},
getProjectReferences: () => {
return commandLine.projectReferences;
},
getProjectVersion: () => {
checkRootFilesUpdate();
return projectVersion.toString();
},
getScriptFileNames: () => {
checkRootFilesUpdate();
return parsedCommandLine.fileNames;
},
getScriptSnapshot: fileName => {
if (!scriptSnapshotsCache.has(fileName)) {
const fileText = ts.sys.readFile(fileName, 'utf8');
if (fileText !== undefined) {
scriptSnapshotsCache.set(fileName, ts.ScriptSnapshot.fromString(fileText));
}
else {
scriptSnapshotsCache.set(fileName, undefined);
}
let fileNames = resolvedFileNameByCommandLine.get(commandLine);
if (!fileNames) {
fileNames = commandLine.fileNames.map(asPosix);
resolvedFileNameByCommandLine.set(commandLine, fileNames);
}
return scriptSnapshotsCache.get(fileName);
return fileNames;
},
};
const languageService = createLanguageService(
language,
languageServicePlugins,
env,
{
typescript: {
configFileName,
sys: ts.sys,
uriConverter: {
asFileName,
asUri,
},
...createLanguageServiceHost(
ts,
ts.sys,
language,
asUri,
projectHost
),
},
}
);

env.onDidChangeWatchedFiles?.(({ changes }) => {
const tsLs: ts.LanguageService = languageService.context.inject('typescript/languageService');
const program = tsLs.getProgram();
for (const change of changes) {
const changeUri = URI.parse(change.uri);
const fileName = asFileName(changeUri);
if (change.type === 2 satisfies typeof FileChangeType.Changed) {
if (scriptSnapshotsCache.has(fileName)) {
if (program?.getSourceFile(fileName)) {
projectVersion++;
scriptSnapshotsCache.delete(fileName);
}
}
else if (change.type === 3 satisfies typeof FileChangeType.Deleted) {
if (scriptSnapshotsCache.has(fileName)) {
if (program?.getSourceFile(fileName)) {
projectVersion++;
scriptSnapshotsCache.delete(fileName);
parsedCommandLine.fileNames = parsedCommandLine.fileNames.filter(name => name !== fileName);
shouldCheckRootFiles = true;
break;
}
}
else if (change.type === 1 satisfies typeof FileChangeType.Created) {
shouldCheckRootFiles = true;
break;
}
}
});

return host;
return [projectHost, languageService] as const;

function checkRootFilesUpdate() {

if (!shouldCheckRootFiles) {
return;
}
shouldCheckRootFiles = false;

const newParsedCommandLine = createParsedCommandLine();
if (!arrayItemsEqual(newParsedCommandLine.fileNames, parsedCommandLine.fileNames)) {
parsedCommandLine.fileNames = newParsedCommandLine.fileNames;
const newCommandLine = getCommandLine();
if (!arrayItemsEqual(newCommandLine.fileNames, commandLine.fileNames)) {
commandLine.fileNames = newCommandLine.fileNames;
projectVersion++;
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/language-core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const defaultMapperFactory: MapperFactory = mappings => new SourceMap(map
export function createLanguage<T>(
plugins: LanguagePlugin<T>[],
scriptRegistry: Map<T, SourceScript<T>>,
sync: (id: T) => void
sync: (id: T, includeFsFiles: boolean) => void
) {
const virtualCodeToSourceScriptMap = new WeakMap<VirtualCode, SourceScript<T>>();
const virtualCodeToSourceMap = new WeakMap<IScriptSnapshot, WeakMap<IScriptSnapshot, Mapper>>();
Expand All @@ -34,8 +34,8 @@ export function createLanguage<T>(
fromVirtualCode(virtualCode) {
return virtualCodeToSourceScriptMap.get(virtualCode)!;
},
get(id) {
sync(id);
get(id, includeFsFiles = true) {
sync(id, includeFsFiles);
const result = scriptRegistry.get(id);
// The sync function provider may not always call the set function due to caching, so it is necessary to explicitly check isAssociationDirty.
if (result?.isAssociationDirty) {
Expand Down Expand Up @@ -220,7 +220,7 @@ export function createLanguage<T>(
sourceScript.isAssociationDirty = false;
return {
getAssociatedScript(id) {
sync(id);
sync(id, true);
const relatedSourceScript = scriptRegistry.get(id);
if (relatedSourceScript) {
relatedSourceScript.targetIds.add(sourceScript.id);
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface Language<T = unknown> {
mapperFactory: MapperFactory;
plugins: LanguagePlugin<T>[];
scripts: {
get(id: T): SourceScript<T> | undefined;
get(id: T, includeFsFiles?: boolean): SourceScript<T> | undefined;
set(id: T, snapshot: IScriptSnapshot, languageId?: string, plugins?: LanguagePlugin<T>[]): SourceScript<T> | undefined;
delete(id: T): void;
fromVirtualCode(virtualCode: VirtualCode): SourceScript<T>;
Expand Down
Loading
Loading