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(typescript): add preset implementations #108

Merged
merged 3 commits into from
Dec 12, 2023
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
1 change: 1 addition & 0 deletions packages/typescript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from './lib/documentRegistry';
export * from './lib/node/decorateLanguageService';
export * from './lib/node/decorateLanguageServiceHost';
export * from './lib/node/decorateProgram';
export * from './lib/node/proxyCreateProgram';
export * from './lib/protocol/createProject';
export * from './lib/protocol/createSys';
171 changes: 171 additions & 0 deletions packages/typescript/lib/node/proxyCreateProgram.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import type * as ts from 'typescript/lib/tsserverlibrary';
import { decorateProgram } from './decorateProgram';
import { LanguagePlugin, createFileProvider, forEachEmbeddedFile, resolveCommonLanguageId } from '@volar/language-core';

export function proxyCreateProgram(
ts: typeof import('typescript'),
original: typeof ts['createProgram'],
extensions: string[],
getLanguagePlugins: (ts: typeof import('typescript/lib/tsserverlibrary'), options: ts.CreateProgramOptions) => LanguagePlugin[],
) {
return new Proxy(original, {
apply: (target, thisArg, args) => {

const options = args[0] as ts.CreateProgramOptions;
assert(!!options.host, '!!options.host');

const originalHost = options.host;

options.host = { ...originalHost };
options.options.allowArbitraryExtensions = true;

const sourceFileToSnapshotMap = new WeakMap<ts.SourceFile, ts.IScriptSnapshot>();
const files = createFileProvider(
getLanguagePlugins(ts, options),
ts.sys.useCaseSensitiveFileNames,
fileName => {
let snapshot: ts.IScriptSnapshot | undefined;
assert(originalSourceFiles.has(fileName), `originalSourceFiles.has(${fileName})`);
const sourceFile = originalSourceFiles.get(fileName);
if (sourceFile) {
snapshot = sourceFileToSnapshotMap.get(sourceFile);
if (!snapshot) {
snapshot = {
getChangeRange() {
return undefined;
},
getLength() {
return sourceFile.text.length;
},
getText(start, end) {
return sourceFile.text.substring(start, end);
},
};
sourceFileToSnapshotMap.set(sourceFile, snapshot);
}
}
if (snapshot) {
files.updateSourceFile(fileName, resolveCommonLanguageId(fileName), snapshot);
}
else {
files.deleteSourceFile(fileName);
}
}
);
const originalSourceFiles = new Map<string, ts.SourceFile | undefined>();
const parsedSourceFiles = new WeakMap<ts.SourceFile, ts.SourceFile>();
const arbitraryExtensions = extensions.map(ext => `.d${ext}.ts`);
const moduleResolutionHost: ts.ModuleResolutionHost = {
...originalHost,
fileExists(fileName) {
for (let i = 0; i < arbitraryExtensions.length; i++) {
if (fileName.endsWith(arbitraryExtensions[i])) {
return originalHost.fileExists(fileName.slice(0, -arbitraryExtensions[i].length) + extensions[i]);
}
}
return originalHost.fileExists(fileName);
},
};

options.host.getSourceFile = (
fileName,
languageVersionOrOptions,
onError,
shouldCreateNewSourceFile,
) => {

const originalSourceFile = originalHost.getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);

originalSourceFiles.set(fileName, originalSourceFile);

if (originalSourceFile && extensions.some(ext => fileName.endsWith(ext))) {
let sourceFile2 = parsedSourceFiles.get(originalSourceFile);
if (!sourceFile2) {
const sourceFile = files.getSourceFile(fileName);
assert(!!sourceFile, '!!sourceFile');
let patchedText = originalSourceFile.text.split('\n').map(line => ' '.repeat(line.length)).join('\n');
let scriptKind = ts.ScriptKind.TS;
const virtualFile = sourceFile.virtualFile?.[0];
if (virtualFile) {
for (const file of forEachEmbeddedFile(virtualFile)) {
if (file.typescript) {
scriptKind = file.typescript.scriptKind;
patchedText += file.snapshot.getText(0, file.snapshot.getLength());
break;
}
}
}
sourceFile2 = ts.createSourceFile(
sourceFile.fileName,
patchedText,
99 satisfies ts.ScriptTarget.ESNext,
true,
scriptKind,
);
// @ts-expect-error
sourceFile2.version = originalSourceFile.version;
parsedSourceFiles.set(originalSourceFile, sourceFile2);
}
return sourceFile2;
}

return originalSourceFile;
};
options.host.resolveModuleNameLiterals = (
moduleNames,
containingFile,
redirectedReference,
options,
) => {
return moduleNames.map<ts.ResolvedModuleWithFailedLookupLocations>(name => {
return resolveModuleName(name.text, containingFile, options, redirectedReference);
});
};
options.host.resolveModuleNames = (
moduleNames,
containingFile,
_reusedNames,
redirectedReference,
options,
) => {
return moduleNames.map<ts.ResolvedModule | undefined>(name => {
return resolveModuleName(name, containingFile, options, redirectedReference).resolvedModule;
});
};

const program = Reflect.apply(target, thisArg, [options]);

decorateProgram(files, program);

return program;

function resolveModuleName(name: string, containingFile: string, options: ts.CompilerOptions, redirectedReference: ts.ResolvedProjectReference | undefined) {
const resolved = ts.resolveModuleName(
name,
containingFile,
options,
moduleResolutionHost,
originalHost.getModuleResolutionCache?.(),
redirectedReference
);
if (resolved.resolvedModule) {
for (let i = 0; i < arbitraryExtensions.length; i++) {
if (resolved.resolvedModule.resolvedFileName.endsWith(arbitraryExtensions[i])) {
const sourceFileName = resolved.resolvedModule.resolvedFileName.slice(0, -arbitraryExtensions[i].length) + extensions[i];
resolved.resolvedModule.resolvedFileName = sourceFileName;
resolved.resolvedModule.extension = extensions[i];
}
}
}
return resolved;
}
},
});
}

function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
console.error(message);
throw new Error(message);
}
}
2 changes: 1 addition & 1 deletion packages/typescript/lib/node/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ function transformRange(
if (filter(sourceStart[1].data)) {
for (const sourceEnd of map.getSourceOffsets(end - sourceFile.snapshot.getLength())) {
if (sourceEnd > sourceStart && filter(sourceEnd[1].data)) {
return [start, end];
return [sourceStart[0], sourceEnd[0]];
}
}
}
Expand Down
103 changes: 103 additions & 0 deletions packages/typescript/lib/starters/createAsyncTSServerPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type * as ts from 'typescript/lib/tsserverlibrary';
import { decorateLanguageService } from '../node/decorateLanguageService';
import { decorateLanguageServiceHost, searchExternalFiles } from '../node/decorateLanguageServiceHost';
import { createFileProvider, LanguagePlugin, resolveCommonLanguageId } from '@volar/language-core';
import { arrayItemsEqual } from './createTSServerPlugin';

const externalFiles = new WeakMap<ts.server.Project, string[]>();

export function createAsyncTSServerPlugin(
extensions: string[],
scriptKind: ts.ScriptKind,
loadLanguagePlugins: (
ts: typeof import('typescript/lib/tsserverlibrary'),
info: ts.server.PluginCreateInfo
) => Promise<LanguagePlugin[]>,
): ts.server.PluginModuleFactory {
return (modules) => {
const { typescript: ts } = modules;
const pluginModule: ts.server.PluginModule = {
create(info) {

const emptySnapshot = ts.ScriptSnapshot.fromString('');
const getScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(info.languageServiceHost);
const getScriptVersion = info.languageServiceHost.getScriptVersion.bind(info.languageServiceHost);
const getScriptKind = info.languageServiceHost.getScriptKind?.bind(info.languageServiceHost);
const getProjectVersion = info.languageServiceHost.getProjectVersion?.bind(info.languageServiceHost);

let initialized = false;

info.languageServiceHost.getScriptSnapshot = (fileName) => {
if (!initialized && extensions.some(ext => fileName.endsWith(ext))) {
return emptySnapshot;
}
return getScriptSnapshot(fileName);
};
info.languageServiceHost.getScriptVersion = (fileName) => {
if (!initialized && extensions.some(ext => fileName.endsWith(ext))) {
return 'initializing...';
}
return getScriptVersion(fileName);
};
if (getScriptKind) {
info.languageServiceHost.getScriptKind = (fileName) => {
if (!initialized && extensions.some(ext => fileName.endsWith(ext))) {
return scriptKind; // TODO: bypass upstream bug
}
return getScriptKind(fileName);
};
}
if (getProjectVersion) {
info.languageServiceHost.getProjectVersion = () => {
if (!initialized) {
return getProjectVersion() + ',initializing...';
}
return getProjectVersion();
};
}

loadLanguagePlugins(ts, info).then(languagePlugins => {
const files = createFileProvider(
languagePlugins,
ts.sys.useCaseSensitiveFileNames,
(fileName) => {
const snapshot = getScriptSnapshot(fileName);
if (snapshot) {
files.updateSourceFile(
fileName,
resolveCommonLanguageId(fileName),
snapshot
);
} else {
files.deleteSourceFile(fileName);
}
}
);

decorateLanguageService(files, info.languageService);
decorateLanguageServiceHost(files, info.languageServiceHost, ts, extensions);

info.project.markAsDirty();
initialized = true;
});

return info.languageService;
},
getExternalFiles(project, updateLevel = 0) {
if (
updateLevel >= (1 satisfies ts.ProgramUpdateLevel.RootNamesAndUpdate)
|| !externalFiles.has(project)
) {
const oldFiles = externalFiles.get(project);
const newFiles = searchExternalFiles(ts, project, extensions);
externalFiles.set(project, newFiles);
if (oldFiles && !arrayItemsEqual(oldFiles, newFiles)) {
project.refreshDiagnostics();
}
}
return externalFiles.get(project)!;
},
};
return pluginModule;
};
}
74 changes: 74 additions & 0 deletions packages/typescript/lib/starters/createTSServerPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type * as ts from 'typescript/lib/tsserverlibrary';
import { decorateLanguageService } from '../node/decorateLanguageService';
import { decorateLanguageServiceHost, searchExternalFiles } from '../node/decorateLanguageServiceHost';
import { createFileProvider, LanguagePlugin, resolveCommonLanguageId } from '@volar/language-core';

const externalFiles = new WeakMap<ts.server.Project, string[]>();
const projectExternalFileExtensions = new WeakMap<ts.server.Project, string[]>();

export function createTSServerPlugin(
init: (
ts: typeof import('typescript/lib/tsserverlibrary'),
info: ts.server.PluginCreateInfo
) => {
languagePlugins: LanguagePlugin[];
extensions: string[];
}
): ts.server.PluginModuleFactory {
return (modules) => {
const { typescript: ts } = modules;
const pluginModule: ts.server.PluginModule = {
create(info) {
const { languagePlugins, extensions } = init(ts, info);
projectExternalFileExtensions.set(info.project, extensions);
const getScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(info.languageServiceHost);
const files = createFileProvider(
languagePlugins,
ts.sys.useCaseSensitiveFileNames,
fileName => {
const snapshot = getScriptSnapshot(fileName);
if (snapshot) {
files.updateSourceFile(fileName, resolveCommonLanguageId(fileName), snapshot);
}
else {
files.deleteSourceFile(fileName);
}
}
);

decorateLanguageService(files, info.languageService);
decorateLanguageServiceHost(files, info.languageServiceHost, ts, extensions);

return info.languageService;
},
getExternalFiles(project, updateLevel = 0) {
if (
updateLevel >= (1 satisfies ts.ProgramUpdateLevel.RootNamesAndUpdate)
|| !externalFiles.has(project)
) {
const oldFiles = externalFiles.get(project);
const newFiles = searchExternalFiles(ts, project, projectExternalFileExtensions.get(project)!);
externalFiles.set(project, newFiles);
if (oldFiles && !arrayItemsEqual(oldFiles, newFiles)) {
project.refreshDiagnostics();
}
}
return externalFiles.get(project)!;
},
};
return pluginModule;
};
}

export function arrayItemsEqual(a: string[], b: string[]) {
if (a.length !== b.length) {
return false;
}
const set = new Set(a);
for (const file of b) {
if (!set.has(file)) {
return false;
}
}
return true;
}
Loading
Loading