Skip to content
Open
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
93 changes: 86 additions & 7 deletions packages/typescript/src/api/async/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ import {
toPath,
} from "../path.ts";
import type {
APIFileChanges,
CompilerOptions,
CreateProgramOptions,
CreateProgramResponse,
Diagnostic,
DocumentIdentifier,
DocumentPosition,
Expand Down Expand Up @@ -135,6 +138,7 @@ import type {
export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts";
export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind };
export type {
APIFileChanges,
APIImportAdderAction as ImportAdderAction,
APIOptions,
AssertsIdentifierTypePredicate,
Expand All @@ -148,6 +152,7 @@ export type {
CompletionInfo,
CompletionOptions,
ConditionalType,
CreateProgramOptions,
Diagnostic,
DocumentIdentifier,
DocumentPosition,
Expand Down Expand Up @@ -387,6 +392,59 @@ export class API<FromLSP extends boolean = false> {
resetTimingInfo(): Promise<void> {
return this.client.resetTimingInfo();
}

private isProgramActive(program: Program): boolean {
const project = program.getProject();
for (const snapshot of this.activeSnapshots) {
if (!snapshot.isDisposed() && snapshot.getProject(project.configFileName)?.program === program) {
return true;
}
}
return false;
}

/**
* Creates a program from current filesystem state, or derives one from oldProgram after applying fileChanges.
*/
async createProgram(
rootFiles: readonly DocumentIdentifier[],
createProgramOptions: CreateProgramOptions,
oldProgram?: Program,
fileChanges?: APIFileChanges,
): Promise<Program> {
await this.ensureInitialized();

if (fileChanges && !oldProgram) {
throw new Error("fileChanges requires an oldProgram");
}
if (oldProgram && !this.isProgramActive(oldProgram)) {
throw new Error("oldProgram must belong to this API instance and reference an active snapshot");
}

const data: CreateProgramResponse = await this.client.apiRequest("createProgram", {
rootFiles,
createProgramOptions,
...(oldProgram ? { oldProgram: { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } } : {}),
...(fileChanges ? { fileChanges } : {}),
Comment thread
gabritto marked this conversation as resolved.
});
if (!data.project) {
throw new Error("createProgram did not return a project");
}
const snapshot = new Snapshot(
{ snapshot: data.snapshot, projects: [data.project] },
this.client,
this.sourceFileCache,
this.toPath!,
() => {
this.activeSnapshots.delete(snapshot);
this.sourceFileCache.releaseSnapshot(snapshot.id);
},
);
const program = snapshot.getProjects()[0].program;
program.setOwnedSnapshot(snapshot);
this.activeSnapshots.add(snapshot);
return program;
}
}

export class InternalAPI {
Expand Down Expand Up @@ -947,13 +1005,15 @@ export class LanguageService {
}

export class Program {
private snapshotId: number;
private project: Project;
private client: Client;
private sourceFileCache: SourceFileCache;
private toPath: (fileName: string) => Path;
private decoder = new Wtf8Decoder();
private sourceFileMetadataCache = new Map<Path, Promise<SourceFileMetadata | undefined>>();
/** @internal */
readonly snapshotId: number;
private readonly project: Project;
private readonly client: Client;
private readonly sourceFileCache: SourceFileCache;
private readonly toPath: (fileName: string) => Path;
private readonly decoder = new Wtf8Decoder();
private readonly sourceFileMetadataCache = new Map<Path, Promise<SourceFileMetadata | undefined>>();
private ownedSnapshot: Snapshot | undefined;

constructor(
snapshotId: number,
Expand All @@ -969,6 +1029,21 @@ export class Program {
this.toPath = toPath;
}

/** @internal */
setOwnedSnapshot(snapshot: Snapshot): void {
this.ownedSnapshot = snapshot;
}

[globalThis.Symbol.dispose](): void {
this.dispose();
}

async dispose(): Promise<void> {
const snapshot = this.ownedSnapshot;
this.ownedSnapshot = undefined;
await snapshot?.dispose();
}

getCompilerOptions(): CompilerOptions {
return this.project.compilerOptions;
}
Expand Down Expand Up @@ -1259,6 +1334,10 @@ export class Program {
});
return toEmitOutput(response);
}

getProject(): Project {
return this.project;
}
}

function toEmitOutput(response: ProtocolEmitOutputResponse): EmitOutput {
Expand Down
24 changes: 24 additions & 0 deletions packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface APIMethodInfo {
initialize: APIMethod<null, InitializeResponse>;
updateSnapshot: APIMethod<UpdateSnapshotParams, UpdateSnapshotResponse>;
updateTemporarySnapshot: APIMethod<UpdateTemporarySnapshotParams, UpdateSnapshotResponse>;
createProgram: APIMethod<CreateProgramParams, CreateProgramResponse>;
parseCommandLine: APIMethod<ParseCommandLineParams, ConfigFileResponse>;
readConfigFile: APIMethod<ReadConfigFileParams, ReadConfigFileResponse>;
parseJsonConfigFileContent: APIMethod<ParseJsonConfigFileContentParams, ConfigFileResponse>;
Expand Down Expand Up @@ -224,6 +225,18 @@ export interface UpdateTemporarySnapshotParams {
newText: string;
}

export interface CreateProgramParams {
rootFiles: readonly DocumentIdentifier[] | null;
createProgramOptions: CreateProgramOptions;
oldProgram?: CreateProgramOldProgramParams;
fileChanges?: APIFileChanges;
}

export interface CreateProgramResponse {
snapshot: number;
project: ProjectResponse | null;
}

export interface ParseCommandLineParams {
commandLine: readonly string[] | null;
}
Expand Down Expand Up @@ -888,6 +901,17 @@ export interface SnapshotChanges {
removedProjects?: string[];
}

export interface CreateProgramOptions {
compilerOptions: CompilerOptions;
projectReferences?: ProjectReference[];
configFileParsingDiagnostics?: DiagnosticResponse[];
}

export interface CreateProgramOldProgramParams {
snapshot?: number;
project?: string;
}

/** CompilerOptions contains the compiler options exposed by the API. */
export interface CompilerOptions {
allowJs?: boolean;
Expand Down
93 changes: 86 additions & 7 deletions packages/typescript/src/api/sync/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ import {
toPath,
} from "../path.ts";
import type {
APIFileChanges,
CompilerOptions,
CreateProgramOptions,
CreateProgramResponse,
Diagnostic,
DocumentIdentifier,
DocumentPosition,
Expand Down Expand Up @@ -143,6 +146,7 @@ import type {
export { documentURIToFileName, fileNameToDocumentURI } from "../path.ts";
export { CheckFlags, CompletionItemKind, DiagnosticCategory, ElementFlags, EmitOnly, ModifierFlags, ModuleKind, NodeBuilderFlags, ObjectFlags, SignatureFlags, SignatureKind, SymbolFlags, TypeFlags, TypeFormatFlags, TypePredicateKind };
export type {
APIFileChanges,
APIImportAdderAction as ImportAdderAction,
APIOptions,
AssertsIdentifierTypePredicate,
Expand All @@ -156,6 +160,7 @@ export type {
CompletionInfo,
CompletionOptions,
ConditionalType,
CreateProgramOptions,
Diagnostic,
DocumentIdentifier,
DocumentPosition,
Expand Down Expand Up @@ -395,6 +400,59 @@ export class API<FromLSP extends boolean = false> {
resetTimingInfo(): void {
return this.client.resetTimingInfo();
}

private isProgramActive(program: Program): boolean {
const project = program.getProject();
for (const snapshot of this.activeSnapshots) {
if (!snapshot.isDisposed() && snapshot.getProject(project.configFileName)?.program === program) {
return true;
}
}
return false;
}

/**
* Creates a program from current filesystem state, or derives one from oldProgram after applying fileChanges.
*/
createProgram(
rootFiles: readonly DocumentIdentifier[],
createProgramOptions: CreateProgramOptions,
oldProgram?: Program,
fileChanges?: APIFileChanges,
): Program {
this.ensureInitialized();

if (fileChanges && !oldProgram) {
throw new Error("fileChanges requires an oldProgram");
}
if (oldProgram && !this.isProgramActive(oldProgram)) {
throw new Error("oldProgram must belong to this API instance and reference an active snapshot");
}

const data: CreateProgramResponse = this.client.apiRequest("createProgram", {
rootFiles,
createProgramOptions,
...(oldProgram ? { oldProgram: { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } } : {}),
...(fileChanges ? { fileChanges } : {}),
Comment thread
gabritto marked this conversation as resolved.
});
if (!data.project) {
throw new Error("createProgram did not return a project");
}
const snapshot = new Snapshot(
{ snapshot: data.snapshot, projects: [data.project] },
this.client,
this.sourceFileCache,
this.toPath!,
() => {
this.activeSnapshots.delete(snapshot);
this.sourceFileCache.releaseSnapshot(snapshot.id);
},
);
const program = snapshot.getProjects()[0].program;
program.setOwnedSnapshot(snapshot);
this.activeSnapshots.add(snapshot);
return program;
}
}

export class InternalAPI {
Expand Down Expand Up @@ -955,13 +1013,15 @@ export class LanguageService {
}

export class Program {
private snapshotId: number;
private project: Project;
private client: Client;
private sourceFileCache: SourceFileCache;
private toPath: (fileName: string) => Path;
private decoder = new Wtf8Decoder();
private sourceFileMetadataCache = new Map<Path, SourceFileMetadata | undefined>();
/** @internal */
readonly snapshotId: number;
private readonly project: Project;
private readonly client: Client;
private readonly sourceFileCache: SourceFileCache;
private readonly toPath: (fileName: string) => Path;
private readonly decoder = new Wtf8Decoder();
private readonly sourceFileMetadataCache = new Map<Path, SourceFileMetadata | undefined>();
private ownedSnapshot: Snapshot | undefined;

constructor(
snapshotId: number,
Expand All @@ -977,6 +1037,21 @@ export class Program {
this.toPath = toPath;
}

/** @internal */
setOwnedSnapshot(snapshot: Snapshot): void {
this.ownedSnapshot = snapshot;
}

[globalThis.Symbol.dispose](): void {
this.dispose();
}

dispose(): void {
const snapshot = this.ownedSnapshot;
this.ownedSnapshot = undefined;
snapshot?.dispose();
}

getCompilerOptions(): CompilerOptions {
return this.project.compilerOptions;
}
Expand Down Expand Up @@ -1267,6 +1342,10 @@ export class Program {
});
return toEmitOutput(response);
}

getProject(): Project {
return this.project;
}
}

function toEmitOutput(response: ProtocolEmitOutputResponse): EmitOutput {
Expand Down
Loading
Loading