diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 2eac81365aae9..71301878ff724 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -56,16 +56,20 @@ import { import type { APIFileChanges, CompilerOptions, + ConfiguredProjectId, CreateProgramOptions, - CreateProgramResponse, + CreateSnapshotParams, + CreateSnapshotResponse, Diagnostic, DocumentIdentifier, DocumentPosition, EmitOutputResponse as ProtocolEmitOutputResponse, ImportAdderAction, + InferredProjectId, IntrinsicTypeMethod, - LSPUpdateSnapshotParams, + LanguageServerSnapshotChanges, ParsedCommandLine, + ProjectId, ProjectReference, ProjectResponse, ReadConfigFileResponse, @@ -75,17 +79,16 @@ import type { SymbolPropertyMethod, SymbolResponse, SymbolsPropertyMethod, + SyntheticProjectId, TextEdit, TypeAcquisition, TypePropertyMethod, TypeResponse, TypesPropertyMethod, - UpdateSnapshotParams, - UpdateSnapshotResponse, } from "../proto.ts"; import { resolveFileName, - toUpdateSnapshotRequest, + toCreateSnapshotRequest, } from "../proto.ts"; import { SourceFileCache } from "../sourceFileCache.ts"; import type { @@ -161,7 +164,9 @@ export type { CompletionInfo, CompletionOptions, ConditionalType, + ConfiguredProjectId, CreateProgramOptions, + CreateSnapshotParams, Diagnostic, DocumentIdentifier, DocumentPosition, @@ -176,15 +181,18 @@ export type { IndexedAccessType, IndexInfo, IndexType, + InferredProjectId, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, + LanguageServerSnapshotChanges, LiteralType, LSPConnectionOptions, NumberLiteralType, ObjectType, ParsedCommandLine, + ProjectId, ProjectReference, ReadConfigFileResponse, RequestTiming, @@ -193,6 +201,7 @@ export type { StringMappingType, StructuredType, SubstitutionType, + SyntheticProjectId, TemplateLiteralType, TextEdit, ThisTypePredicate, @@ -236,7 +245,6 @@ export class API implements FormatDiagnosticsHo private initialized: boolean = false; private initializing: Promise | undefined; private activeSnapshots: Set = new Set(); - private latestSnapshot: Snapshot | undefined; readonly internal: InternalAPI; constructor(options: APIOptions | LSPConnectionOptions = {}) { @@ -350,40 +358,96 @@ export class API implements FormatDiagnosticsHo return this.client.apiRequest("transpileDeclarationFromFile", { fileName: resolveFileName(file), options }); } - async updateSnapshot(params?: FromLSP extends true ? LSPUpdateSnapshotParams : UpdateSnapshotParams): Promise { - return this.updateSnapshotWorker(params); + createSnapshot(params: Params): Promise>; + createSnapshot(): Promise; + async createSnapshot(params?: CreateSnapshotParams): Promise { + await this.ensureInitialized(); + + const requestParams = toCreateSnapshotRequest(params); + const data = await this.client.apiRequest("createSnapshot", requestParams); + + const snapshot = new Snapshot( + data, + this.client, + this.sourceFileCache, + this.toPath!, + this, + () => { + this.activeSnapshots.delete(snapshot); + this.sourceFileCache.releaseSnapshot(snapshot.id); + }, + this.createSnapshotUpdater(() => snapshot), + undefined, + ); + this.activeSnapshots.add(snapshot); + + return snapshot; } - /** @internal */ - async updateSnapshotFrom(baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Promise { + private async updateSnapshot(baseSnapshot: Snapshot, params?: CreateSnapshotParams): Promise { + await this.ensureInitialized(); if (!this.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { throw new Error("Cannot update an inactive snapshot"); } - if (baseSnapshot !== this.latestSnapshot) { - // TODO: Support forking active memory/cache snapshots once the server-side - // ownership, project state, and cache semantics have been worked out. - throw new Error("Snapshot.update can only update the latest snapshot"); - } - return this.updateSnapshotWorker(params, baseSnapshot); + + const data = await this.client.apiRequest("updateSnapshot", { + snapshot: baseSnapshot.id, + changes: toCreateSnapshotRequest(params), + }); + this.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); + const snapshot = new Snapshot( + data, + this.client, + this.sourceFileCache, + this.toPath!, + this, + () => { + this.activeSnapshots.delete(snapshot); + this.sourceFileCache.releaseSnapshot(snapshot.id); + }, + this.createSnapshotUpdater(() => snapshot), + baseSnapshot, + ); + this.activeSnapshots.add(snapshot); + return snapshot; } - private async updateSnapshotWorker( - params?: LSPUpdateSnapshotParams | UpdateSnapshotParams, - baseSnapshot?: Snapshot, + private createSnapshotUpdater(getSnapshot: () => Snapshot): SnapshotUpdater { + const update: SnapshotUpdater = params => this.updateSnapshot(getSnapshot(), params); // @sync: const update = ((params?: CreateSnapshotParams) => this.updateSnapshot(getSnapshot(), params)) as SnapshotUpdater; + // @sync-only-start + // const owner = this; + // update.gen = function* (params?: CreateSnapshotParams) { return yield* owner.updateSnapshot.gen(getSnapshot(), params); }; + // @sync-only-end + return update; + } + + /** + * Returns the language server's current canonical snapshot after atomically + * adopting any supplied API-driven changes. Only available on LSP-connected APIs. + */ + getCurrentLanguageServerSnapshot( + ...args: FromLSP extends true ? [changes: Params, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] + ): Promise>; + getCurrentLanguageServerSnapshot( + ...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] + ): Promise; + async getCurrentLanguageServerSnapshot( + ...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never] ): Promise { await this.ensureInitialized(); - const requestParams = toUpdateSnapshotRequest(params, baseSnapshot?.id); - const data = await this.client.apiRequest("updateSnapshot", requestParams); - - // Retain cached source files from previous snapshot for unchanged files - if (this.latestSnapshot) { - this.sourceFileCache.retainForSnapshot(data.snapshot, this.latestSnapshot.id, data.changes); - if (this.latestSnapshot.isDisposed()) { - this.sourceFileCache.releaseSnapshot(this.latestSnapshot.id); - } + const changes = args[0] as LanguageServerSnapshotChanges | undefined; + const baseSnapshot = args[1] as Snapshot | undefined; + if (baseSnapshot && (!this.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed())) { + throw new Error("Cannot use an inactive snapshot as a response base"); + } + const data = await this.client.apiRequest("getCurrentLanguageServerSnapshot", { + ...(baseSnapshot ? { baseSnapshot: baseSnapshot.id } : {}), + ...(changes ? { changes } : {}), + }); + if (baseSnapshot) { + this.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); } - const snapshot = new Snapshot( data, this.client, @@ -392,14 +456,12 @@ export class API implements FormatDiagnosticsHo this, () => { this.activeSnapshots.delete(snapshot); - if (snapshot !== this.latestSnapshot) { - this.sourceFileCache.releaseSnapshot(snapshot.id); - } + this.sourceFileCache.releaseSnapshot(snapshot.id); }, + this.createSnapshotUpdater(() => snapshot), + baseSnapshot, ); - this.latestSnapshot = snapshot; this.activeSnapshots.add(snapshot); - return snapshot; } @@ -414,11 +476,6 @@ export class API implements FormatDiagnosticsHo for (const snapshot of [...this.activeSnapshots]) { await snapshot.dispose(); } - // Release the latest snapshot's cache refs if still held - if (this.latestSnapshot) { - this.sourceFileCache.releaseSnapshot(this.latestSnapshot.id); - this.latestSnapshot = undefined; - } this.sourceFileCache.clear(); } finally { @@ -453,6 +510,8 @@ export class API implements FormatDiagnosticsHo this.activeSnapshots.delete(snapshot); this.sourceFileCache.releaseSnapshot(snapshot.id); }, + this.createSnapshotUpdater(() => snapshot), + baseSnapshot, ); this.activeSnapshots.add(snapshot); @@ -484,67 +543,28 @@ export class API implements FormatDiagnosticsHo 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. - */ + /** Creates a program from current filesystem state. */ async createProgram( rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions, - oldProgram?: Program, - fileChanges?: APIFileChanges, ): Promise { 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 } : {}), + const snapshot = await this.createSnapshot({ + createPrograms: [{ rootFiles, options: createProgramOptions }], }); - if (!data.project) { + const program = snapshot.operation.createdPrograms[0]; + if (!program) { + await snapshot.dispose(); 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, - () => { - this.activeSnapshots.delete(snapshot); - this.sourceFileCache.releaseSnapshot(snapshot.id); - }, - ); - const program = snapshot.getProjects()[0].program; program.setOwnedSnapshot(snapshot); - this.activeSnapshots.add(snapshot); return program; } } type EnsureInitialized = () => Promise; // @sync: type EnsureInitialized = (() => void) & { gen(): Generator; }; -interface SnapshotOwner extends FormatDiagnosticsHost { - updateSnapshotFrom(baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Promise; -} - export class InternalAPI { private client: Client; private ensureInitialized: EnsureInitialized; @@ -573,39 +593,77 @@ export class InternalAPI { } } +type SnapshotUpdater = (params?: CreateSnapshotParams) => Promise; // @sync: type SnapshotUpdater = ((params?: CreateSnapshotParams) => Snapshot) & { gen(params?: CreateSnapshotParams): Generator; }; + +export interface SnapshotOperation { + readonly createdPrograms?: readonly Program[]; + readonly openedFiles?: readonly SnapshotOpenedFileOperation[]; +} + +export interface SnapshotOpenedFileOperation { + readonly project: Project; +} + +type MapTupleTo = { + readonly [Index in keyof Tuple]: Result; +}; + +export type SnapshotForOperation = Snapshot & { + readonly operation: + & SnapshotOperation + & (Params extends { createPrograms: infer Programs extends readonly unknown[]; } ? { readonly createdPrograms: MapTupleTo>; } : unknown) + & (Params extends { openFiles: infer Files extends readonly unknown[]; } ? { readonly openedFiles: MapTupleTo; } : unknown); +}; + export class Snapshot { readonly id: number; - private projectMap: Map; + readonly operation: SnapshotOperation; + private projectMap: Map; private toPath: (fileName: string) => Path; private client: Client; private disposed: boolean = false; private disposePromise: Promise | undefined; private onDispose: () => void; - private api: SnapshotOwner; private snapshotRegistry: SnapshotObjectRegistry; + private projectDataMap: Map; + private updateSnapshot: SnapshotUpdater; readonly internal: SnapshotInternalAPI; constructor( - data: UpdateSnapshotResponse, + data: CreateSnapshotResponse, client: Client, sourceFileCache: SourceFileCache, toPath: (fileName: string) => Path, - api: SnapshotOwner, + formatDiagnosticsHost: FormatDiagnosticsHost, onDispose: () => void, + updateSnapshot: SnapshotUpdater, + baseSnapshot?: Snapshot, ) { this.id = data.snapshot; this.client = client; this.toPath = toPath; - this.api = api; this.onDispose = onDispose; + this.updateSnapshot = updateSnapshot; this.projectMap = new Map(); + this.projectDataMap = new Map(baseSnapshot?.projectDataMap); + for (const projectId of data.changes?.removedProjects ?? []) { + this.projectDataMap.delete(projectId); + } + for (const projectData of data.projects) { + this.projectDataMap.set(projectData.id, projectData); + } this.snapshotRegistry = new SnapshotObjectRegistry(client, this.id, projectId => this.projectMap.get(projectId)); - for (const projData of data.projects) { - const project = new Project(projData, this.id, client, sourceFileCache, toPath, api, this.snapshotRegistry); - this.projectMap.set(toPath(projData.configFileName), project); + for (const projData of this.projectDataMap.values()) { + const project = new Project(projData, this.id, client, sourceFileCache, toPath, formatDiagnosticsHost, this.snapshotRegistry); + this.projectMap.set(projData.id, project); } + this.operation = { + ...(data.operation.createdPrograms ? { createdPrograms: data.operation.createdPrograms.map(projectId => this.requireProject(projectId).program) } : {}), + ...(data.operation.openedFiles ? { openedFiles: data.operation.openedFiles.map(result => ({ project: this.requireProject(result.project) })) } : {}), + }; + this.internal = new SnapshotInternalAPI(this.id, client); } @@ -614,11 +672,33 @@ export class Snapshot { return [...this.projectMap.values()]; } - getProject(configFileName: string): Project | undefined { + getConfiguredProject(configFileName: string): Project | undefined { + this.ensureNotDisposed(); + return this.projectMap.get(this.toPath(configFileName) as ConfiguredProjectId) as Project | undefined; + } + + getProject(projectId: Id): Project | undefined { + this.ensureNotDisposed(); + return this.projectMap.get(projectId) as Project | undefined; + } + + getProgram(projectId: Id): Program | undefined { + return this.getProject(projectId)?.program; + } + + update(params: Params): Promise>; + update(): Promise; + update(params?: CreateSnapshotParams): Promise { this.ensureNotDisposed(); - return this.projectMap.get(this.toPath(configFileName)); + return this.updateSnapshot(params); } + /** + * Gets the default project for a given file from the configured projects and + * inferred project already loaded in the snapshot. Synthetic projects are not + * considered. Files that have been opened with `openFiles` are guaranteed to + * have a result. + */ async getDefaultProjectForFile(file: DocumentIdentifier): Promise { this.ensureNotDisposed(); const data = await this.client.apiRequest("getDefaultProjectForFile", { @@ -626,16 +706,7 @@ export class Snapshot { file, }); if (!data) return undefined; - return this.projectMap.get(this.toPath(data.configFileName)); - } - - /** - * Creates the next snapshot, layering its filesystem over this snapshot's - * filesystem. This snapshot must still be active and be the latest snapshot. - */ - async update(params?: UpdateSnapshotParams): Promise { - this.ensureNotDisposed(); - return this.api.updateSnapshotFrom(this, params); + return this.projectMap.get(data.id); } [globalThis.Symbol.dispose](): void { @@ -670,22 +741,30 @@ export class Snapshot { throw new Error("Snapshot is disposed"); } } + + private requireProject(projectId: Id): Project { + const project = this.projectMap.get(projectId); + if (!project) { + throw new Error(`Snapshot operation returned unknown project '${projectId}'`); + } + return project as Project; + } } class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; private readonly snapshotId: number; - private readonly resolveProject: (projectId: Path) => Project | undefined; + private readonly resolveProject: (projectId: ProjectId) => Project | undefined; - constructor(client: Client, snapshotId: number, resolveProject: (projectId: Path) => Project | undefined) { + constructor(client: Client, snapshotId: number, resolveProject: (projectId: ProjectId) => Project | undefined) { this.client = client; this.snapshotId = snapshotId; this.resolveProject = resolveProject; } - /** Resolve a project id (a config file path) to its Project within this snapshot. */ - getProject(projectId: Path): Project | undefined { + /** Resolve a project ID to its Project within this snapshot. */ + getProject(projectId: ProjectId): Project | undefined { return this.resolveProject(projectId); } @@ -706,7 +785,7 @@ class SnapshotObjectRegistry { this.symbols.clear(); } - async fetchSymbol(source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Promise { + async fetchSymbol(source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: ProjectId): Promise { if (!handle) return undefined as unknown as Symbol; const cached = this.getSymbol(handle); if (cached) return cached; @@ -720,7 +799,7 @@ class SnapshotObjectRegistry { return this.getOrCreateSymbol(data); } - async fetchSymbols(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): Promise { + async fetchSymbols(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: ProjectId): Promise { if (handles) { const result = new Array(handles.length); let allCached = true; @@ -950,17 +1029,18 @@ class ProjectObjectRegistry { } } -export class Project { - readonly id: Path; +export class Project { + readonly id: Id; readonly configFileName: string; readonly currentDirectory: string; + readonly dirty: boolean; readonly parsedCommandLine: ParsedCommandLine; /** @deprecated Use `parsedCommandLine.options`. */ readonly compilerOptions: CompilerOptions; /** @deprecated Use `parsedCommandLine.fileNames`. */ readonly rootFiles: readonly string[]; - readonly program: Program; + readonly program: Program; readonly checker: Checker; readonly emitter: Emitter; readonly languageService: LanguageService; @@ -976,9 +1056,10 @@ export class Project { formatDiagnosticsHost: FormatDiagnosticsHost, snapshotRegistry: SnapshotObjectRegistry, ) { - this.id = data.id as Path; + this.id = data.id as Id; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; + this.dirty = data.dirty; if (!data.parsedCommandLine?.options) { throw new Error(`Project '${data.configFileName}' has no parsed command line`); } @@ -1130,10 +1211,11 @@ export class LanguageService { } } -export class Program implements FormatDiagnosticsHost { +export class Program implements FormatDiagnosticsHost { /** @internal */ readonly snapshotId: number; - private readonly project: Project; + readonly id: Id; + private readonly project: Project; private readonly client: Client; private readonly sourceFileCache: SourceFileCache; private readonly toPath: (fileName: string) => Path; @@ -1145,13 +1227,14 @@ export class Program implements FormatDiagnosticsHost { constructor( snapshotId: number, - project: Project, + project: Project, client: Client, sourceFileCache: SourceFileCache, toPath: (fileName: string) => Path, formatDiagnosticsHost: FormatDiagnosticsHost, ) { this.snapshotId = snapshotId; + this.id = project.id; this.project = project; this.client = client; this.sourceFileCache = sourceFileCache; @@ -1487,7 +1570,7 @@ export class Program implements FormatDiagnosticsHost { return toEmitOutput(response); } - getProject(): Project { + getProject(): Project { return this.project; } } @@ -2424,7 +2507,7 @@ export class Symbol { this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; this.checkFlags = data.checkFlags; - const canonicalProject = objectRegistry.getProject(data.project as Path); + const canonicalProject = objectRegistry.getProject(data.project); if (!canonicalProject) { throw new Error(`Symbol ${data.id} references unknown canonical project '${data.project}'`); } diff --git a/packages/typescript/src/api/proto.generated.ts b/packages/typescript/src/api/proto.generated.ts index b207862c5705b..be0a86800016a 100644 --- a/packages/typescript/src/api/proto.generated.ts +++ b/packages/typescript/src/api/proto.generated.ts @@ -6,6 +6,7 @@ import { ModuleKind } from "#enums/moduleKind"; import { ModuleResolutionKind } from "#enums/moduleResolutionKind"; import { NewLineKind } from "#enums/newLineKind"; import { ScriptTarget } from "#enums/scriptTarget"; +import type { Path } from "../ast/index.ts"; export { JsxEmit } from "#enums/jsxEmit"; export { ModuleDetectionKind } from "#enums/moduleDetectionKind"; @@ -20,9 +21,10 @@ export interface APIMethodInfo { release: APIMethod; batchRequests: APIMethod; initialize: APIMethod; - updateSnapshot: APIMethod; - updateTemporarySnapshot: APIMethod; - createProgram: APIMethod; + createSnapshot: APIMethod; + updateSnapshot: APIMethod; + getCurrentLanguageServerSnapshot: APIMethod; + updateTemporarySnapshot: APIMethod; parseCommandLine: APIMethod; readConfigFile: APIMethod; parseJsonConfigFileContent: APIMethod; @@ -163,6 +165,13 @@ export interface APIMethodInfo { export type DocumentIdentifier = string | { uri: string; }; +export type EnsurePrograms = true | readonly ProjectId[]; + +export type InferredProjectId = string & { __inferredProjectIdBrand: any; }; +export type ConfiguredProjectId = Path & { __configuredProjectIdBrand: any; }; +export type SyntheticProjectId = string & { __syntheticProjectIdBrand: any; }; +export type ProjectId = InferredProjectId | ConfiguredProjectId | SyntheticProjectId; + /** ReleaseParams are the parameters for the release method. */ export interface ReleaseParams { snapshot: number; @@ -187,61 +196,41 @@ export interface InitializeResponse { currentDirectory: string; } -/** - * UpdateSnapshotParams are the parameters for creating a new snapshot. - * All fields are optional. With no fields set, the server adopts the latest LSP state. - */ -export interface UpdateSnapshotParams { - /** - * Snapshot, when set, requires this to be the latest active snapshot and layers - * FileSystem over that snapshot's filesystem. Used by Snapshot.update. - */ - snapshot?: number; - /** - * OpenProjects lists tsconfig.json files to open/load in the new snapshot. - * Opens are ref-counted and persist across snapshots until closed. - */ - openProjects?: readonly DocumentIdentifier[]; - /** - * CloseProjects lists tsconfig.json files to release in the new snapshot. - * A project is only unloaded once every API client that opened it closes it. - */ - closeProjects?: readonly DocumentIdentifier[]; - /** FileChanges describes file system changes since the last snapshot. */ +/** CreateSnapshotParams are the parameters for creating a new independent snapshot. */ +export interface CreateSnapshotParams extends SnapshotRequestChangesParams { + /** FileChanges describes host file system changes to invalidate while creating the snapshot. */ fileChanges?: APIFileChanges; /** * FileSystem supplies file contents and directory listings for the new snapshot. * A full filesystem is canonical and total. A filesystem layer is checked - * before falling back to the host filesystem. + * before falling back to the base snapshot or host filesystem. */ fileSystem?: RequestFileSystem; - /** - * OpenFiles lists files to keep open for the API client, mirroring LSP's - * textDocument/didOpen. For each file, ancestor directories are searched for a - * tsconfig that contains it; if found, that configured project is loaded and - * becomes the file's default project. Otherwise the file is loaded into the - * inferred project (e.g. a node_modules d.ts not in any project's import graph). - * Opens persist across snapshots until the file is closed. - */ - openFiles?: readonly DocumentIdentifier[]; - /** - * CloseFiles lists files to release in the new snapshot. A file is only fully - * closed once every API client that opened it closes it. - */ - closeFiles?: readonly DocumentIdentifier[]; } -/** UpdateSnapshotResponse is returned by updateSnapshot. */ -export interface UpdateSnapshotResponse { +/** CreateSnapshotResponse is returned by createSnapshot. */ +export interface CreateSnapshotResponse { /** Snapshot is the handle for the newly created snapshot. */ snapshot: number; - /** Projects is the list of projects in the snapshot. */ - projects: ProjectResponse[]; /** - * Changes describes source file differences from the previous snapshot. - * Nil for the first snapshot in a session. + * Projects contains all projects when no response base was supplied, or only + * projects added or replaced relative to that base. */ + projects: ProjectResponse[]; + /** Changes describes source file differences from the response base. */ changes?: SnapshotChanges; + /** Operation describes results correlated with the request that produced the snapshot. */ + operation: SnapshotOperationResponse; +} + +export interface UpdateSnapshotParams { + snapshot: number; + changes?: CreateSnapshotParams; +} + +export interface GetCurrentLanguageServerSnapshotParams { + baseSnapshot?: number; + changes?: LanguageServerSnapshotChanges; } /** @@ -257,18 +246,6 @@ 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; } @@ -324,9 +301,10 @@ export interface GetDefaultProjectForFileParams { } export interface ProjectResponse { - id: string; + id: ProjectId; configFileName: string; currentDirectory: string; + dirty: boolean; parsedCommandLine: ConfigFileResponse; /** @deprecated Use parsedCommandLine.fileNames. */ rootFiles: string[]; @@ -336,7 +314,7 @@ export interface ProjectResponse { export interface GetSymbolAtPositionParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; position: number; } @@ -347,7 +325,7 @@ export interface SymbolResponse { * Project is the project in which the symbol was first observed. It is the * default project for follow-up lookups whose results can vary by project. */ - project: string; + project: ProjectId; name: string; flags: number; checkFlags: number; @@ -359,38 +337,38 @@ export interface SymbolResponse { export interface GetSymbolsAtPositionsParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; positions: readonly number[] | null; } export interface GetSymbolAtLocationParams { snapshot: number; - project: string; + project: ProjectId; location: string; } export interface GetSymbolsAtLocationsParams { snapshot: number; - project: string; + project: ProjectId; locations: readonly string[] | null; } export interface GetSymbolOfSourceFileParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; } export interface GetSymbolsOfSourceFilesParams { snapshot: number; - project: string; + project: ProjectId; files: readonly DocumentIdentifier[] | null; } export interface GetTypeOfSymbolParams { snapshot: number; - project: string; + project: ProjectId; symbol: number; } @@ -442,13 +420,13 @@ export interface TypeResponse { export interface GetTypesOfSymbolsParams { snapshot: number; - project: string; + project: ProjectId; symbols: readonly number[] | null; } export interface GetSourceFileParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; } @@ -463,7 +441,7 @@ export interface SourceFileResponse { export interface GetSourceFileNamesParams { snapshot: number; - project: string; + project: ProjectId; } /** SourceFileMetadata carries program-stored metadata about a single source file. */ @@ -478,12 +456,12 @@ export interface SourceFileMetadata { /** GetProjectDiagnosticsParams are parameters for project-wide diagnostic methods. */ export interface GetProjectDiagnosticsParams { snapshot: number; - project: string; + project: ProjectId; } export interface ResolveNameParams { snapshot: number; - project: string; + project: ProjectId; name: string; /** Optional: node handle for location context */ location?: string; @@ -503,7 +481,7 @@ export interface ResolveNameParams { */ export interface GetSymbolsInScopeParams { snapshot: number; - project: string; + project: ProjectId; /** Optional: node handle for location context */ location?: string; /** Optional: file for location context (alternative to Location) */ @@ -516,7 +494,7 @@ export interface GetSymbolsInScopeParams { export interface GetSignaturesOfTypeParams { snapshot: number; - project: string; + project: ProjectId; type: number; kind: number; } @@ -533,32 +511,32 @@ export interface SignatureResponse { export interface GetResolvedSignatureParams { snapshot: number; - project: string; + project: ProjectId; location: string; } export interface GetTypeAtLocationParams { snapshot: number; - project: string; + project: ProjectId; location: string; } export interface GetTypeAtLocationsParams { snapshot: number; - project: string; + project: ProjectId; locations: readonly string[] | null; } export interface GetTypeAtPositionParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; position: number; } export interface GetTypesAtPositionsParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; positions: readonly number[] | null; } @@ -566,56 +544,56 @@ export interface GetTypesAtPositionsParams { /** GetSymbolPropertyParams is used for all symbol sub-property endpoints. */ export interface GetSymbolPropertyParams { snapshot: number; - project: string; + project: ProjectId; objectId: number; } /** GetTypePropertyParams is used for all type sub-property endpoints. */ export interface GetTypePropertyParams { snapshot: number; - project: string; + project: ProjectId; objectId: number; } /** GetSignaturePropertyParams is used for all signature sub-property endpoints. */ export interface GetSignaturePropertyParams { snapshot: number; - project: string; + project: ProjectId; objectId: number; } /** GetContextualTypeParams returns the contextual type for a node. */ export interface GetContextualTypeParams { snapshot: number; - project: string; + project: ProjectId; location: string; } /** GetBaseTypeOfLiteralTypeParams returns the base type of a literal type. */ export interface GetBaseTypeOfLiteralTypeParams { snapshot: number; - project: string; + project: ProjectId; type: number; } /** GetTypeFromTypeNodeParams are the parameters for the getTypeFromTypeNode method. */ export interface GetTypeFromTypeNodeParams { snapshot: number; - project: string; + project: ProjectId; location: string; } /** GetWidenedTypeParams are the parameters for the getWidenedType method. */ export interface GetWidenedTypeParams { snapshot: number; - project: string; + project: ProjectId; type: number; } /** GetParameterTypeParams are the parameters for the getParameterType method. */ export interface GetParameterTypeParams { snapshot: number; - project: string; + project: ProjectId; signature: number; index: number; } @@ -623,14 +601,14 @@ export interface GetParameterTypeParams { /** IsArrayLikeTypeParams checks whether a type is array-like. */ export interface IsArrayLikeTypeParams { snapshot: number; - project: string; + project: ProjectId; type: number; } /** IsTypeAssignableToParams checks assignability between two types. */ export interface IsTypeAssignableToParams { snapshot: number; - project: string; + project: ProjectId; source: number; target: number; } @@ -638,7 +616,7 @@ export interface IsTypeAssignableToParams { /** GetTypeOfSymbolAtLocationParams returns the narrowed type of a symbol at a specific location. */ export interface GetTypeOfSymbolAtLocationParams { snapshot: number; - project: string; + project: ProjectId; symbol: number; location: string; } @@ -646,7 +624,7 @@ export interface GetTypeOfSymbolAtLocationParams { /** TypeToTypeNodeParams are the parameters for the typeToTypeNode method. */ export interface TypeToTypeNodeParams { snapshot: number; - project: string; + project: ProjectId; type: number; location?: string; flags?: number; @@ -655,7 +633,7 @@ export interface TypeToTypeNodeParams { /** SignatureToSignatureDeclarationParams are the parameters for the signatureToSignatureDeclaration method. */ export interface SignatureToSignatureDeclarationParams { snapshot: number; - project: string; + project: ProjectId; signature: number; kind: number; location?: string; @@ -665,7 +643,7 @@ export interface SignatureToSignatureDeclarationParams { /** CheckerSignatureParams are parameters for checker methods that operate on a signature. */ export interface CheckerSignatureParams { snapshot: number; - project: string; + project: ProjectId; signature: number; } @@ -680,14 +658,14 @@ export interface TypePredicateResponse { /** CheckerTypeParams are parameters for checker methods that operate on a type. */ export interface CheckerTypeParams { snapshot: number; - project: string; + project: ProjectId; type: number; } /** GetPropertyOfTypeParams are parameters for getPropertyOfType (a named property of a type). */ export interface GetPropertyOfTypeParams { snapshot: number; - project: string; + project: ProjectId; type: number; name: string; } @@ -702,7 +680,7 @@ export interface IndexInfoResponse { export interface GetImportAdderEditsParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; actions: readonly ImportAdderAction[] | null; } @@ -716,21 +694,21 @@ export interface TextEdit { /** CheckerNodeParams are parameters for checker methods that operate on a node location. */ export interface CheckerNodeParams { snapshot: number; - project: string; + project: ProjectId; location: string; } /** CheckerSymbolParams are parameters for checker methods that operate on a symbol. */ export interface CheckerSymbolParams { snapshot: number; - project: string; + project: ProjectId; symbol: number; } /** GetMemberInModuleExportsParams are parameters for getMemberInModuleExports. */ export interface GetMemberInModuleExportsParams { snapshot: number; - project: string; + project: ProjectId; symbol: number; name: string; } @@ -747,7 +725,7 @@ export interface JSDocTagInfo { /** GetReferencesToSymbolInFileParams are the parameters for the getReferencesToSymbolInFile method. */ export interface GetReferencesToSymbolInFileParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; symbol: number; } @@ -755,7 +733,7 @@ export interface GetReferencesToSymbolInFileParams { /** GetReferencedSymbolsForNodeParams are the parameters for the getReferencedSymbolsForNode method. */ export interface GetReferencedSymbolsForNodeParams { snapshot: number; - project: string; + project: ProjectId; node: string; position: number; } @@ -770,7 +748,7 @@ export interface ReferencedSymbolEntry { /** GetSignatureUsagesParams are the parameters for the getSignatureUsages method. */ export interface GetSignatureUsagesParams { snapshot: number; - project: string; + project: ProjectId; signatureDecl: string; } @@ -783,7 +761,7 @@ export interface SignatureUsageResponse { /** GetCompletionsAtPositionParams are the parameters for the getCompletionsAtPosition method. */ export interface GetCompletionsAtPositionParams { snapshot: number; - project: string; + project: ProjectId; file: DocumentIdentifier; position: number; triggerCharacter?: string; @@ -799,7 +777,7 @@ export interface CompletionInfoResponse { /** GetDiagnosticsParams are parameters for per-file diagnostic methods. */ export interface GetDiagnosticsParams { snapshot: number; - project: string; + project: ProjectId; files?: readonly DocumentIdentifier[]; } @@ -847,7 +825,7 @@ export interface PrintNodeParams { /** FormatNodeForInsertionParams are the parameters for the formatNodeForInsertion method. */ export interface FormatNodeForInsertionParams { snapshot: number; - project: string; + project: ProjectId; /** target file where the node will be inserted */ file: DocumentIdentifier; /** UTF-16 code-unit offset of the insertion position in the target file */ @@ -858,7 +836,7 @@ export interface FormatNodeForInsertionParams { export interface EmitParams { snapshot: number; - project: string; + project: ProjectId; emitOnly?: number; } @@ -881,14 +859,14 @@ export interface EmitOutputResponse { export interface SelectedFilesEmitParams { snapshot: number; - project: string; + project: ProjectId; files: readonly DocumentIdentifier[] | null; } /** GetIntrinsicTypeParams is used for intrinsic type getters (anyType, stringType, etc.). */ export interface GetIntrinsicTypeParams { snapshot: number; - project: string; + project: ProjectId; } /** @@ -922,7 +900,7 @@ export interface ProfileResult { export interface BatchRequest { method: | "batchRequests" - | "createProgram" + | "createSnapshot" | "emit" | "emitToString" | "formatNodeForInsertion" @@ -948,6 +926,7 @@ export interface BatchRequest { | "getConstraintOfType" | "getConstraintOfTypeParameter" | "getContextualType" + | "getCurrentLanguageServerSnapshot" | "getDeclarationDiagnostics" | "getDeclarationEmit" | "getDeclaredTypeOfSymbol" @@ -1069,7 +1048,7 @@ export interface BatchRequest { export interface BatchResponse { method: | "batchRequests" - | "createProgram" + | "createSnapshot" | "emit" | "emitToString" | "formatNodeForInsertion" @@ -1095,6 +1074,7 @@ export interface BatchResponse { | "getConstraintOfType" | "getConstraintOfTypeParameter" | "getContextualType" + | "getCurrentLanguageServerSnapshot" | "getDeclarationDiagnostics" | "getDeclarationEmit" | "getDeclaredTypeOfSymbol" @@ -1214,6 +1194,42 @@ export interface BatchResponse { error?: string; } +/** + * SnapshotRequestChangesParams describes project, file, and program changes to apply + * while creating or updating a snapshot. + */ +export interface SnapshotRequestChangesParams { + /** OpenProjects lists tsconfig.json files to open/load in the new snapshot. */ + openProjects?: readonly DocumentIdentifier[]; + /** + * CloseProjects lists tsconfig.json files to release in the new snapshot. + * A project is only unloaded once every API client that opened it closes it. + */ + closeProjects?: readonly DocumentIdentifier[]; + /** + * OpenFiles lists files to open in the new snapshot, mirroring LSP's + * textDocument/didOpen. For each file, ancestor directories are searched for a + * tsconfig that contains it; if found, that configured project is loaded and + * becomes the file's default project. Otherwise the file is loaded into the + * inferred project (e.g. a node_modules d.ts not in any project's import graph). + */ + openFiles?: readonly DocumentIdentifier[]; + /** + * CloseFiles lists files to release in the new snapshot. A file is only fully + * closed once every API client that opened it closes it. + */ + closeFiles?: readonly DocumentIdentifier[]; + /** CreatePrograms describes synthetic programs to create in the snapshot. */ + createPrograms?: readonly CreateSnapshotProgramParams[]; + /** RemovePrograms lists synthetic project handles to remove from the snapshot. */ + removePrograms?: readonly SyntheticProjectId[]; + /** + * EnsurePrograms identifies projects whose programs should be updated if dirty, + * or all contained projects when true. + */ + ensurePrograms?: EnsurePrograms; +} + /** * APIFileChanges describes file changes to apply when updating a snapshot. * Either InvalidateAll is true (discard all caches) or Changed/Created/Deleted @@ -1246,8 +1262,8 @@ export interface RequestFileSystem { } /** - * SnapshotChanges describes what changed between the previous latest snapshot - * and the newly created snapshot. Changes are reported per-project so clients + * SnapshotChanges describes what changed between a response base and a new + * snapshot. Changes are reported per-project so clients * can track cache refs at the (snapshot, project) level. */ export interface SnapshotChanges { @@ -1260,18 +1276,19 @@ export interface SnapshotChanges { * RemovedProjects lists project handles that were present in the previous * snapshot but absent from the new one. */ - removedProjects?: string[]; + removedProjects?: ProjectId[]; } -export interface CreateProgramOptions { - compilerOptions: CompilerOptions; - projectReferences?: ProjectReference[]; - configFileParsingDiagnostics?: DiagnosticResponse[]; +export interface SnapshotOperationResponse { + createdPrograms?: SyntheticProjectId[]; + openedFiles?: OpenedFileOperationResult[]; } -export interface CreateProgramOldProgramParams { - snapshot?: number; - project?: string; +/** + * LanguageServerSnapshotChanges describes API-driven changes to adopt into the + * language server's canonical state. + */ +export interface LanguageServerSnapshotChanges extends SnapshotRequestChangesParams { } /** CompilerOptions contains the compiler options exposed by the API. */ @@ -1436,6 +1453,11 @@ export interface EmitOutputFile { sourceFileName?: string; } +export interface CreateSnapshotProgramParams { + rootFiles: readonly DocumentIdentifier[] | null; + options: CreateProgramOptions; +} + /** * RequestDirectoryEntries is a cached directory listing. Entry names are * relative to the directory, matching vfs.GetAccessibleEntries. @@ -1467,8 +1489,18 @@ export interface ProjectFileChanges { deletedFiles?: string[]; } +export interface OpenedFileOperationResult { + project: ProjectId; +} + /** CompletionEntryLabelDetailsResponse holds additional label display text for a completion entry. */ export interface CompletionEntryLabelDetailsResponse { detail?: string; description?: string; } + +export interface CreateProgramOptions { + compilerOptions: CompilerOptions; + projectReferences?: ProjectReference[]; + configFileParsingDiagnostics?: DiagnosticResponse[]; +} diff --git a/packages/typescript/src/api/proto.ts b/packages/typescript/src/api/proto.ts index 6c45235499142..228ded96c06ad 100644 --- a/packages/typescript/src/api/proto.ts +++ b/packages/typescript/src/api/proto.ts @@ -4,12 +4,12 @@ import { } from "./path.ts"; import type { APIMethodInfo, + CreateSnapshotParams as CoreCreateSnapshotParams, DocumentIdentifier, SignatureResponse, SourceFileResponse, SymbolResponse, TypeResponse, - UpdateSnapshotParams as CoreUpdateSnapshotParams, } from "./proto.generated.ts"; export type { ConfigFileResponse as ParsedCommandLine, DiagnosticResponse as Diagnostic } from "./proto.generated.ts"; @@ -81,21 +81,10 @@ export function resolveDocumentURI(identifier: DocumentIdentifier): string { return identifier.uri; } -export interface LSPUpdateSnapshotParams extends Omit { - /** - * @deprecated Use {@link openProjects} instead. - * Path to a tsconfig.json file to open in the new snapshot. - */ - openProject?: string; - - /** FileChanges are not supplied by the LSP */ - fileChanges?: never; -} - /** - * Parameters for updateSnapshot, including deprecated members handled by `toUpdateSnapshotRequest` + * Parameters for createSnapshot, including deprecated members handled by `toCreateSnapshotRequest` */ -export interface UpdateSnapshotParams extends Omit { +export interface CreateSnapshotParams extends CoreCreateSnapshotParams { /** * @deprecated Use {@link openProjects} instead. * Path to a tsconfig.json file to open in the new snapshot. @@ -104,18 +93,17 @@ export interface UpdateSnapshotParams extends Omit(changes?.removedProjects ?? []); const changedProjects = changes?.changedProjects ?? {}; for (const [projectId, paths] of prevProjectMap) { diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 642a41740c3f2..3b0c97b67a46f 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -73,16 +73,20 @@ import { import type { APIFileChanges, CompilerOptions, + ConfiguredProjectId, CreateProgramOptions, - CreateProgramResponse, + CreateSnapshotParams, + CreateSnapshotResponse, Diagnostic, DocumentIdentifier, DocumentPosition, EmitOutputResponse as ProtocolEmitOutputResponse, ImportAdderAction, + InferredProjectId, IntrinsicTypeMethod, - LSPUpdateSnapshotParams, + LanguageServerSnapshotChanges, ParsedCommandLine, + ProjectId, ProjectReference, ProjectResponse, ReadConfigFileResponse, @@ -92,17 +96,16 @@ import type { SymbolPropertyMethod, SymbolResponse, SymbolsPropertyMethod, + SyntheticProjectId, TextEdit, TypeAcquisition, TypePropertyMethod, TypeResponse, TypesPropertyMethod, - UpdateSnapshotParams, - UpdateSnapshotResponse, } from "../proto.ts"; import { resolveFileName, - toUpdateSnapshotRequest, + toCreateSnapshotRequest, } from "../proto.ts"; import { SourceFileCache } from "../sourceFileCache.ts"; import type { @@ -178,7 +181,9 @@ export type { CompletionInfo, CompletionOptions, ConditionalType, + ConfiguredProjectId, CreateProgramOptions, + CreateSnapshotParams, Diagnostic, DocumentIdentifier, DocumentPosition, @@ -193,15 +198,18 @@ export type { IndexedAccessType, IndexInfo, IndexType, + InferredProjectId, InterfaceType, IntersectionType, IntrinsicType, JSDocTagInfo, + LanguageServerSnapshotChanges, LiteralType, LSPConnectionOptions, NumberLiteralType, ObjectType, ParsedCommandLine, + ProjectId, ProjectReference, ReadConfigFileResponse, RequestTiming, @@ -210,6 +218,7 @@ export type { StringMappingType, StructuredType, SubstitutionType, + SyntheticProjectId, TemplateLiteralType, TextEdit, ThisTypePredicate, @@ -255,7 +264,6 @@ export class API implements FormatDiagnosticsHo private initialized: boolean = false; private initializing: void | undefined; private activeSnapshots: Set = new Set(); - private latestSnapshot: Snapshot | undefined; readonly internal: InternalAPI; constructor(options: APIOptions | LSPConnectionOptions = {}) { @@ -543,79 +551,85 @@ export class API implements FormatDiagnosticsHo ); } - get updateSnapshot(): { - (params?: FromLSP extends true ? LSPUpdateSnapshotParams : UpdateSnapshotParams): Snapshot; - gen(params?: FromLSP extends true ? LSPUpdateSnapshotParams : UpdateSnapshotParams): Generator; + get createSnapshot(): { + (params: Params): SnapshotForOperation; + (): Snapshot; + gen(params: Params): Generator, ProtocolResponse["result"]>; + gen(): Generator; } { const owner = this; - return cacheGeneratorMethod( - owner, - "updateSnapshot", - function (params?: FromLSP extends true ? LSPUpdateSnapshotParams : UpdateSnapshotParams): Snapshot { - return owner.updateSnapshotWorker(params); - }, - function* (params?: FromLSP extends true ? LSPUpdateSnapshotParams : UpdateSnapshotParams): Generator { - return yield* owner.updateSnapshotWorker.gen(params); - }, - ); - } + function createSnapshot(params: Params): SnapshotForOperation; + function createSnapshot(): Snapshot; + function createSnapshot(params?: CreateSnapshotParams): Snapshot { + owner.ensureInitialized(); - /** @internal */ - get updateSnapshotFrom(): { - (baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Snapshot; - gen(baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "updateSnapshotFrom", - function (baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Snapshot { - if (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { - throw new Error("Cannot update an inactive snapshot"); - } - if (baseSnapshot !== owner.latestSnapshot) { - // TODO: Support forking active memory/cache snapshots once the server-side - // ownership, project state, and cache semantics have been worked out. - throw new Error("Snapshot.update can only update the latest snapshot"); - } - return owner.updateSnapshotWorker(params, baseSnapshot); - }, - function* (baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Generator { - if (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { - throw new Error("Cannot update an inactive snapshot"); - } - if (baseSnapshot !== owner.latestSnapshot) { - // TODO: Support forking active memory/cache snapshots once the server-side - // ownership, project state, and cache semantics have been worked out. - throw new Error("Snapshot.update can only update the latest snapshot"); - } - return yield* owner.updateSnapshotWorker.gen(params, baseSnapshot); - }, - ); + const requestParams = toCreateSnapshotRequest(params); + const data = owner.client.apiRequest("createSnapshot", requestParams); + + const snapshot = new Snapshot( + data, + owner.client, + owner.sourceFileCache, + owner.toPath!, + owner, + () => { + owner.activeSnapshots.delete(snapshot); + owner.sourceFileCache.releaseSnapshot(snapshot.id); + }, + owner.createSnapshotUpdater(() => snapshot), + undefined, + ); + owner.activeSnapshots.add(snapshot); + + return snapshot; + } + function gen(params: Params): Generator, ProtocolResponse["result"]>; + function gen(): Generator; + function* gen(params?: CreateSnapshotParams): Generator { + yield* owner.ensureInitialized.gen(); + + const requestParams = toCreateSnapshotRequest(params); + const data = yield* apiRequest("createSnapshot", requestParams); + + const snapshot = new Snapshot( + data, + owner.client, + owner.sourceFileCache, + owner.toPath!, + owner, + () => { + owner.activeSnapshots.delete(snapshot); + owner.sourceFileCache.releaseSnapshot(snapshot.id); + }, + owner.createSnapshotUpdater(() => snapshot), + undefined, + ); + owner.activeSnapshots.add(snapshot); + + return snapshot; + } + return cacheGeneratorMethod(owner, "createSnapshot", createSnapshot, gen); } - private get updateSnapshotWorker(): { - (params?: LSPUpdateSnapshotParams | UpdateSnapshotParams, baseSnapshot?: Snapshot): Snapshot; - gen(params?: LSPUpdateSnapshotParams | UpdateSnapshotParams, baseSnapshot?: Snapshot): Generator; + private get updateSnapshot(): { + (baseSnapshot: Snapshot, params?: CreateSnapshotParams): Snapshot; + gen(baseSnapshot: Snapshot, params?: CreateSnapshotParams): Generator; } { const owner = this; return cacheGeneratorMethod( owner, - "updateSnapshotWorker", - function (params?: LSPUpdateSnapshotParams | UpdateSnapshotParams, baseSnapshot?: Snapshot): Snapshot { + "updateSnapshot", + function (baseSnapshot: Snapshot, params?: CreateSnapshotParams): Snapshot { owner.ensureInitialized(); - - const requestParams = toUpdateSnapshotRequest(params, baseSnapshot?.id); - const data = owner.client.apiRequest("updateSnapshot", requestParams); - - // Retain cached source files from previous snapshot for unchanged files - if (owner.latestSnapshot) { - owner.sourceFileCache.retainForSnapshot(data.snapshot, owner.latestSnapshot.id, data.changes); - if (owner.latestSnapshot.isDisposed()) { - owner.sourceFileCache.releaseSnapshot(owner.latestSnapshot.id); - } + if (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { + throw new Error("Cannot update an inactive snapshot"); } + const data = owner.client.apiRequest("updateSnapshot", { + snapshot: baseSnapshot.id, + changes: toCreateSnapshotRequest(params), + }); + owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); const snapshot = new Snapshot( data, owner.client, @@ -624,30 +638,25 @@ export class API implements FormatDiagnosticsHo owner, () => { owner.activeSnapshots.delete(snapshot); - if (snapshot !== owner.latestSnapshot) { - owner.sourceFileCache.releaseSnapshot(snapshot.id); - } + owner.sourceFileCache.releaseSnapshot(snapshot.id); }, + owner.createSnapshotUpdater(() => snapshot), + baseSnapshot, ); - owner.latestSnapshot = snapshot; owner.activeSnapshots.add(snapshot); - return snapshot; }, - function* (params?: LSPUpdateSnapshotParams | UpdateSnapshotParams, baseSnapshot?: Snapshot): Generator { + function* (baseSnapshot: Snapshot, params?: CreateSnapshotParams): Generator { yield* owner.ensureInitialized.gen(); - - const requestParams = toUpdateSnapshotRequest(params, baseSnapshot?.id); - const data = yield* apiRequest("updateSnapshot", requestParams); - - // Retain cached source files from previous snapshot for unchanged files - if (owner.latestSnapshot) { - owner.sourceFileCache.retainForSnapshot(data.snapshot, owner.latestSnapshot.id, data.changes); - if (owner.latestSnapshot.isDisposed()) { - owner.sourceFileCache.releaseSnapshot(owner.latestSnapshot.id); - } + if (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed()) { + throw new Error("Cannot update an inactive snapshot"); } + const data = yield* apiRequest("updateSnapshot", { + snapshot: baseSnapshot.id, + changes: toCreateSnapshotRequest(params), + }); + owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); const snapshot = new Snapshot( data, owner.client, @@ -656,19 +665,106 @@ export class API implements FormatDiagnosticsHo owner, () => { owner.activeSnapshots.delete(snapshot); - if (snapshot !== owner.latestSnapshot) { - owner.sourceFileCache.releaseSnapshot(snapshot.id); - } + owner.sourceFileCache.releaseSnapshot(snapshot.id); }, + owner.createSnapshotUpdater(() => snapshot), + baseSnapshot, ); - owner.latestSnapshot = snapshot; owner.activeSnapshots.add(snapshot); - return snapshot; }, ); } + private createSnapshotUpdater(getSnapshot: () => Snapshot): SnapshotUpdater { + const update = ((params?: CreateSnapshotParams) => this.updateSnapshot(getSnapshot(), params)) as SnapshotUpdater; + const owner = this; + update.gen = function* (params?: CreateSnapshotParams) { + return yield* owner.updateSnapshot.gen(getSnapshot(), params); + }; + return update; + } + + /** + * Returns the language server's current canonical snapshot after atomically + * adopting any supplied API-driven changes. Only available on LSP-connected APIs. + */ + get getCurrentLanguageServerSnapshot(): { + (...args: FromLSP extends true ? [changes: Params, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): SnapshotForOperation; + (...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Snapshot; + gen(...args: FromLSP extends true ? [changes: Params, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Generator, ProtocolResponse["result"]>; + gen(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Generator; + } { + const owner = this; + function getCurrentLanguageServerSnapshot(...args: FromLSP extends true ? [changes: Params, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): SnapshotForOperation; + function getCurrentLanguageServerSnapshot(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Snapshot; + function getCurrentLanguageServerSnapshot(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Snapshot { + owner.ensureInitialized(); + + const changes = args[0] as LanguageServerSnapshotChanges | undefined; + const baseSnapshot = args[1] as Snapshot | undefined; + if (baseSnapshot && (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed())) { + throw new Error("Cannot use an inactive snapshot as a response base"); + } + const data = owner.client.apiRequest("getCurrentLanguageServerSnapshot", { + ...(baseSnapshot ? { baseSnapshot: baseSnapshot.id } : {}), + ...(changes ? { changes } : {}), + }); + if (baseSnapshot) { + owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); + } + const snapshot = new Snapshot( + data, + owner.client, + owner.sourceFileCache, + owner.toPath!, + owner, + () => { + owner.activeSnapshots.delete(snapshot); + owner.sourceFileCache.releaseSnapshot(snapshot.id); + }, + owner.createSnapshotUpdater(() => snapshot), + baseSnapshot, + ); + owner.activeSnapshots.add(snapshot); + return snapshot; + } + function gen(...args: FromLSP extends true ? [changes: Params, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Generator, ProtocolResponse["result"]>; + function gen(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Generator; + function* gen(...args: FromLSP extends true ? [changes?: LanguageServerSnapshotChanges, baseSnapshot?: Snapshot] : [changes: never, baseSnapshot?: never]): Generator { + yield* owner.ensureInitialized.gen(); + + const changes = args[0] as LanguageServerSnapshotChanges | undefined; + const baseSnapshot = args[1] as Snapshot | undefined; + if (baseSnapshot && (!owner.activeSnapshots.has(baseSnapshot) || baseSnapshot.isDisposed())) { + throw new Error("Cannot use an inactive snapshot as a response base"); + } + const data = yield* apiRequest("getCurrentLanguageServerSnapshot", { + ...(baseSnapshot ? { baseSnapshot: baseSnapshot.id } : {}), + ...(changes ? { changes } : {}), + }); + if (baseSnapshot) { + owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); + } + const snapshot = new Snapshot( + data, + owner.client, + owner.sourceFileCache, + owner.toPath!, + owner, + () => { + owner.activeSnapshots.delete(snapshot); + owner.sourceFileCache.releaseSnapshot(snapshot.id); + }, + owner.createSnapshotUpdater(() => snapshot), + baseSnapshot, + ); + owner.activeSnapshots.add(snapshot); + return snapshot; + } + return cacheGeneratorMethod(owner, "getCurrentLanguageServerSnapshot", getCurrentLanguageServerSnapshot, gen); + } + [globalThis.Symbol.dispose](): void { this.close(); } @@ -687,11 +783,6 @@ export class API implements FormatDiagnosticsHo for (const snapshot of [...owner.activeSnapshots]) { snapshot.dispose(); } - // Release the latest snapshot's cache refs if still held - if (owner.latestSnapshot) { - owner.sourceFileCache.releaseSnapshot(owner.latestSnapshot.id); - owner.latestSnapshot = undefined; - } owner.sourceFileCache.clear(); } finally { @@ -704,11 +795,6 @@ export class API implements FormatDiagnosticsHo for (const snapshot of [...owner.activeSnapshots]) { yield* snapshot.dispose.gen(); } - // Release the latest snapshot's cache refs if still held - if (owner.latestSnapshot) { - owner.sourceFileCache.releaseSnapshot(owner.latestSnapshot.id); - owner.latestSnapshot = undefined; - } owner.sourceFileCache.clear(); } finally { @@ -753,6 +839,8 @@ export class API implements FormatDiagnosticsHo owner.activeSnapshots.delete(snapshot); owner.sourceFileCache.releaseSnapshot(snapshot.id); }, + owner.createSnapshotUpdater(() => snapshot), + baseSnapshot, ); owner.activeSnapshots.add(snapshot); @@ -786,6 +874,8 @@ export class API implements FormatDiagnosticsHo owner.activeSnapshots.delete(snapshot); owner.sourceFileCache.releaseSnapshot(snapshot.id); }, + owner.createSnapshotUpdater(() => snapshot), + baseSnapshot, ); owner.activeSnapshots.add(snapshot); @@ -845,95 +935,41 @@ export class API implements FormatDiagnosticsHo ); } - 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. - */ + /** Creates a program from current filesystem state. */ get createProgram(): { - (rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions, oldProgram?: Program, fileChanges?: APIFileChanges): Program; - gen(rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions, oldProgram?: Program, fileChanges?: APIFileChanges): Generator; + (rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions): Program; + gen(rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "createProgram", - function (rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions, oldProgram?: Program, fileChanges?: APIFileChanges): Program { + function (rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions): Program { owner.ensureInitialized(); - if (fileChanges && !oldProgram) { - throw new Error("fileChanges requires an oldProgram"); - } - if (oldProgram && !owner.isProgramActive(oldProgram)) { - throw new Error("oldProgram must belong to this API instance and reference an active snapshot"); - } - - const data: CreateProgramResponse = owner.client.apiRequest("createProgram", { - rootFiles, - createProgramOptions, - ...(oldProgram ? { oldProgram: { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } } : {}), - ...(fileChanges ? { fileChanges } : {}), + const snapshot = owner.createSnapshot({ + createPrograms: [{ rootFiles, options: createProgramOptions }], }); - if (!data.project) { + const program = snapshot.operation.createdPrograms[0]; + if (!program) { + snapshot.dispose(); throw new Error("createProgram did not return a project"); } - const snapshot = new Snapshot( - { snapshot: data.snapshot, projects: [data.project] }, - owner.client, - owner.sourceFileCache, - owner.toPath!, - owner, - () => { - owner.activeSnapshots.delete(snapshot); - owner.sourceFileCache.releaseSnapshot(snapshot.id); - }, - ); - const program = snapshot.getProjects()[0].program; program.setOwnedSnapshot(snapshot); - owner.activeSnapshots.add(snapshot); return program; }, - function* (rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions, oldProgram?: Program, fileChanges?: APIFileChanges): Generator { + function* (rootFiles: readonly DocumentIdentifier[], createProgramOptions: CreateProgramOptions): Generator { yield* owner.ensureInitialized.gen(); - if (fileChanges && !oldProgram) { - throw new Error("fileChanges requires an oldProgram"); - } - if (oldProgram && !owner.isProgramActive(oldProgram)) { - throw new Error("oldProgram must belong to this API instance and reference an active snapshot"); - } - - const data: CreateProgramResponse = yield* apiRequest("createProgram", { - rootFiles, - createProgramOptions, - ...(oldProgram ? { oldProgram: { snapshot: oldProgram.snapshotId, project: oldProgram.getProject().id } } : {}), - ...(fileChanges ? { fileChanges } : {}), + const snapshot = yield* owner.createSnapshot.gen({ + createPrograms: [{ rootFiles, options: createProgramOptions }], }); - if (!data.project) { + const program = snapshot.operation.createdPrograms[0]; + if (!program) { + yield* snapshot.dispose.gen(); throw new Error("createProgram did not return a project"); } - const snapshot = new Snapshot( - { snapshot: data.snapshot, projects: [data.project] }, - owner.client, - owner.sourceFileCache, - owner.toPath!, - owner, - () => { - owner.activeSnapshots.delete(snapshot); - owner.sourceFileCache.releaseSnapshot(snapshot.id); - }, - ); - const program = snapshot.getProjects()[0].program; program.setOwnedSnapshot(snapshot); - owner.activeSnapshots.add(snapshot); return program; }, ); @@ -942,13 +978,6 @@ export class API implements FormatDiagnosticsHo type EnsureInitialized = (() => void) & { gen(): Generator; }; -interface SnapshotOwner extends FormatDiagnosticsHost { - updateSnapshotFrom: { - (baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Snapshot; - gen(baseSnapshot: Snapshot, params?: UpdateSnapshotParams): Generator; - }; -} - export class InternalAPI { private client: Client; private ensureInitialized: EnsureInitialized; @@ -1021,39 +1050,77 @@ export class InternalAPI { } } +type SnapshotUpdater = ((params?: CreateSnapshotParams) => Snapshot) & { gen(params?: CreateSnapshotParams): Generator; }; + +export interface SnapshotOperation { + readonly createdPrograms?: readonly Program[]; + readonly openedFiles?: readonly SnapshotOpenedFileOperation[]; +} + +export interface SnapshotOpenedFileOperation { + readonly project: Project; +} + +type MapTupleTo = { + readonly [Index in keyof Tuple]: Result; +}; + +export type SnapshotForOperation = Snapshot & { + readonly operation: + & SnapshotOperation + & (Params extends { createPrograms: infer Programs extends readonly unknown[]; } ? { readonly createdPrograms: MapTupleTo>; } : unknown) + & (Params extends { openFiles: infer Files extends readonly unknown[]; } ? { readonly openedFiles: MapTupleTo; } : unknown); +}; + export class Snapshot { readonly id: number; - private projectMap: Map; + readonly operation: SnapshotOperation; + private projectMap: Map; private toPath: (fileName: string) => Path; private client: Client; private disposed: boolean = false; private disposePromise: void | undefined; private onDispose: () => void; - private api: SnapshotOwner; private snapshotRegistry: SnapshotObjectRegistry; + private projectDataMap: Map; + private updateSnapshot: SnapshotUpdater; readonly internal: SnapshotInternalAPI; constructor( - data: UpdateSnapshotResponse, + data: CreateSnapshotResponse, client: Client, sourceFileCache: SourceFileCache, toPath: (fileName: string) => Path, - api: SnapshotOwner, + formatDiagnosticsHost: FormatDiagnosticsHost, onDispose: () => void, + updateSnapshot: SnapshotUpdater, + baseSnapshot?: Snapshot, ) { this.id = data.snapshot; this.client = client; this.toPath = toPath; - this.api = api; this.onDispose = onDispose; + this.updateSnapshot = updateSnapshot; this.projectMap = new Map(); + this.projectDataMap = new Map(baseSnapshot?.projectDataMap); + for (const projectId of data.changes?.removedProjects ?? []) { + this.projectDataMap.delete(projectId); + } + for (const projectData of data.projects) { + this.projectDataMap.set(projectData.id, projectData); + } this.snapshotRegistry = new SnapshotObjectRegistry(client, this.id, projectId => this.projectMap.get(projectId)); - for (const projData of data.projects) { - const project = new Project(projData, this.id, client, sourceFileCache, toPath, api, this.snapshotRegistry); - this.projectMap.set(toPath(projData.configFileName), project); + for (const projData of this.projectDataMap.values()) { + const project = new Project(projData, this.id, client, sourceFileCache, toPath, formatDiagnosticsHost, this.snapshotRegistry); + this.projectMap.set(projData.id, project); } + this.operation = { + ...(data.operation.createdPrograms ? { createdPrograms: data.operation.createdPrograms.map(projectId => this.requireProject(projectId).program) } : {}), + ...(data.operation.openedFiles ? { openedFiles: data.operation.openedFiles.map(result => ({ project: this.requireProject(result.project) })) } : {}), + }; + this.internal = new SnapshotInternalAPI(this.id, client); } @@ -1062,11 +1129,48 @@ export class Snapshot { return [...this.projectMap.values()]; } - getProject(configFileName: string): Project | undefined { + getConfiguredProject(configFileName: string): Project | undefined { this.ensureNotDisposed(); - return this.projectMap.get(this.toPath(configFileName)); + return this.projectMap.get(this.toPath(configFileName) as ConfiguredProjectId) as Project | undefined; + } + + getProject(projectId: Id): Project | undefined { + this.ensureNotDisposed(); + return this.projectMap.get(projectId) as Project | undefined; + } + + getProgram(projectId: Id): Program | undefined { + return this.getProject(projectId)?.program; } + get update(): { + (params: Params): SnapshotForOperation; + (): Snapshot; + gen(params: Params): Generator, ProtocolResponse["result"]>; + gen(): Generator; + } { + const owner = this; + function update(params: Params): SnapshotForOperation; + function update(): Snapshot; + function update(params?: CreateSnapshotParams): Snapshot { + owner.ensureNotDisposed(); + return owner.updateSnapshot(params); + } + function gen(params: Params): Generator, ProtocolResponse["result"]>; + function gen(): Generator; + function* gen(params?: CreateSnapshotParams): Generator { + owner.ensureNotDisposed(); + return yield* owner.updateSnapshot.gen(params); + } + return cacheGeneratorMethod(owner, "update", update, gen); + } + + /** + * Gets the default project for a given file from the configured projects and + * inferred project already loaded in the snapshot. Synthetic projects are not + * considered. Files that have been opened with `openFiles` are guaranteed to + * have a result. + */ get getDefaultProjectForFile(): { (file: DocumentIdentifier): Project | undefined; gen(file: DocumentIdentifier): Generator; @@ -1082,7 +1186,7 @@ export class Snapshot { file, }); if (!data) return undefined; - return owner.projectMap.get(owner.toPath(data.configFileName)); + return owner.projectMap.get(data.id); }, function* (file: DocumentIdentifier): Generator { owner.ensureNotDisposed(); @@ -1091,30 +1195,7 @@ export class Snapshot { file, }); if (!data) return undefined; - return owner.projectMap.get(owner.toPath(data.configFileName)); - }, - ); - } - - /** - * Creates the next snapshot, layering its filesystem over this snapshot's - * filesystem. This snapshot must still be active and be the latest snapshot. - */ - get update(): { - (params?: UpdateSnapshotParams): Snapshot; - gen(params?: UpdateSnapshotParams): Generator; - } { - const owner = this; - return cacheGeneratorMethod( - owner, - "update", - function (params?: UpdateSnapshotParams): Snapshot { - owner.ensureNotDisposed(); - return owner.api.updateSnapshotFrom(owner, params); - }, - function* (params?: UpdateSnapshotParams): Generator { - owner.ensureNotDisposed(); - return yield* owner.api.updateSnapshotFrom.gen(owner, params); + return owner.projectMap.get(data.id); }, ); } @@ -1189,22 +1270,30 @@ export class Snapshot { throw new Error("Snapshot is disposed"); } } + + private requireProject(projectId: Id): Project { + const project = this.projectMap.get(projectId); + if (!project) { + throw new Error(`Snapshot operation returned unknown project '${projectId}'`); + } + return project as Project; + } } class SnapshotObjectRegistry { private readonly symbols: Map = new Map(); private readonly client: Client; private readonly snapshotId: number; - private readonly resolveProject: (projectId: Path) => Project | undefined; + private readonly resolveProject: (projectId: ProjectId) => Project | undefined; - constructor(client: Client, snapshotId: number, resolveProject: (projectId: Path) => Project | undefined) { + constructor(client: Client, snapshotId: number, resolveProject: (projectId: ProjectId) => Project | undefined) { this.client = client; this.snapshotId = snapshotId; this.resolveProject = resolveProject; } - /** Resolve a project id (a config file path) to its Project within this snapshot. */ - getProject(projectId: Path): Project | undefined { + /** Resolve a project ID to its Project within this snapshot. */ + getProject(projectId: ProjectId): Project | undefined { return this.resolveProject(projectId); } @@ -1226,14 +1315,14 @@ class SnapshotObjectRegistry { } get fetchSymbol(): { - (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Symbol; - gen(source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Generator; + (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: ProjectId): Symbol; + gen(source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: ProjectId): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "fetchSymbol", - function (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Symbol { + function (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: ProjectId): Symbol { if (!handle) return undefined as unknown as Symbol; const cached = owner.getSymbol(handle); if (cached) return cached; @@ -1246,7 +1335,7 @@ class SnapshotObjectRegistry { if (!data) throw new Error(`${method} returned null symbol for ${source.constructor.name} ${source.id}`); return owner.getOrCreateSymbol(data); }, - function* (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: Path): Generator { + function* (source: Symbol | Signature | Type, method: SymbolPropertyMethod, handle: number | undefined, projectId: ProjectId): Generator { if (!handle) return undefined as unknown as Symbol; const cached = owner.getSymbol(handle); if (cached) return cached; @@ -1263,14 +1352,14 @@ class SnapshotObjectRegistry { } get fetchSymbols(): { - (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): readonly Symbol[]; - gen(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): Generator; + (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: ProjectId): readonly Symbol[]; + gen(source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: ProjectId): Generator; } { const owner = this; return cacheGeneratorMethod( owner, "fetchSymbols", - function (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): readonly Symbol[] { + function (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: ProjectId): readonly Symbol[] { if (handles) { const result = new Array(handles.length); let allCached = true; @@ -1292,7 +1381,7 @@ class SnapshotObjectRegistry { if (symbolData == null) return []; else return symbolData.map(data => owner.getOrCreateSymbol(data)); }, - function* (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: Path): Generator { + function* (source: Symbol | Signature | Type, method: SymbolsPropertyMethod, handles: readonly number[] | undefined, projectId: ProjectId): Generator { if (handles) { const result = new Array(handles.length); let allCached = true; @@ -1781,17 +1870,18 @@ class ProjectObjectRegistry { } } -export class Project { - readonly id: Path; +export class Project { + readonly id: Id; readonly configFileName: string; readonly currentDirectory: string; + readonly dirty: boolean; readonly parsedCommandLine: ParsedCommandLine; /** @deprecated Use `parsedCommandLine.options`. */ readonly compilerOptions: CompilerOptions; /** @deprecated Use `parsedCommandLine.fileNames`. */ readonly rootFiles: readonly string[]; - readonly program: Program; + readonly program: Program; readonly checker: Checker; readonly emitter: Emitter; readonly languageService: LanguageService; @@ -1807,9 +1897,10 @@ export class Project { formatDiagnosticsHost: FormatDiagnosticsHost, snapshotRegistry: SnapshotObjectRegistry, ) { - this.id = data.id as Path; + this.id = data.id as Id; this.configFileName = data.configFileName; this.currentDirectory = data.currentDirectory; + this.dirty = data.dirty; if (!data.parsedCommandLine?.options) { throw new Error(`Project '${data.configFileName}' has no parsed command line`); } @@ -2122,10 +2213,11 @@ export class LanguageService { } } -export class Program implements FormatDiagnosticsHost { +export class Program implements FormatDiagnosticsHost { /** @internal */ readonly snapshotId: number; - private readonly project: Project; + readonly id: Id; + private readonly project: Project; private readonly client: Client; private readonly sourceFileCache: SourceFileCache; private readonly toPath: (fileName: string) => Path; @@ -2137,13 +2229,14 @@ export class Program implements FormatDiagnosticsHost { constructor( snapshotId: number, - project: Project, + project: Project, client: Client, sourceFileCache: SourceFileCache, toPath: (fileName: string) => Path, formatDiagnosticsHost: FormatDiagnosticsHost, ) { this.snapshotId = snapshotId; + this.id = project.id; this.project = project; this.client = client; this.sourceFileCache = sourceFileCache; @@ -2917,7 +3010,7 @@ export class Program implements FormatDiagnosticsHost { ); } - getProject(): Project { + getProject(): Project { return this.project; } } @@ -5251,7 +5344,7 @@ export class Symbol { this.name = unescapeLeadingUnderscores(data.name as __String); this.flags = data.flags; this.checkFlags = data.checkFlags; - const canonicalProject = objectRegistry.getProject(data.project as Path); + const canonicalProject = objectRegistry.getProject(data.project); if (!canonicalProject) { throw new Error(`Symbol ${data.id} references unknown canonical project '${data.project}'`); } diff --git a/packages/typescript/test/api-comparison.bench.ts b/packages/typescript/test/api-comparison.bench.ts index 233ed8487a7f9..e515cb383ee14 100644 --- a/packages/typescript/test/api-comparison.bench.ts +++ b/packages/typescript/test/api-comparison.bench.ts @@ -357,7 +357,7 @@ export async function runBenchmarks(options?: { filter?: string; singleIteration function createSyncContext(): SyncContext { const api = new SyncAPI({ cwd: repoRoot }); - const snapshot = api.updateSnapshot({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" }); + const snapshot = api.createSnapshot({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" }); const project = snapshot.getProjects()[0]; project.checker.getSymbolAtPosition("core.ts", 0); return { api, project }; @@ -365,7 +365,7 @@ export async function runBenchmarks(options?: { filter?: string; singleIteration async function createAsyncContext(): Promise { const api = new AsyncAPI({ cwd: repoRoot }); - const snapshot = await api.updateSnapshot({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" }); + const snapshot = await api.createSnapshot({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" }); const project = snapshot.getProjects()[0]; await project.checker.getSymbolAtPosition("core.ts", 0); return { api, project }; @@ -373,7 +373,7 @@ export async function runBenchmarks(options?: { filter?: string; singleIteration function createGeneratorContext(): SyncContext { const api = new SyncAPI({ cwd: repoRoot }); - const [snapshot] = api.batch(api.updateSnapshot.gen({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" })); + const [snapshot] = api.batch(api.createSnapshot.gen({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" })); const project = snapshot.getProjects()[0]; api.batch(project.checker.getSymbolAtPosition.gen("core.ts", 0)); return { api, project }; diff --git a/packages/typescript/test/async/api.bench.ts b/packages/typescript/test/async/api.bench.ts index 48c2380a76d05..01196f4cece46 100644 --- a/packages/typescript/test/async/api.bench.ts +++ b/packages/typescript/test/async/api.bench.ts @@ -206,7 +206,7 @@ export async function runBenchmarks(options?: { filter?: string; singleIteration } async function loadSnapshot() { - snapshot = await api.updateSnapshot({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" }); // @generators: [snapshot] = api.batch(api.updateSnapshot.gen({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" })); + snapshot = await api.createSnapshot({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" }); // @generators: [snapshot] = api.batch(api.createSnapshot.gen({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" })); project = snapshot.getProjects()[0]; } diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 9d0d352061ea2..5620aa1de53a4 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -28,6 +28,7 @@ import { type Node, type NodeArray, NodeFlags, + type Path, SyntaxKind, tryGetAmbientModuleNameFromSymbolName, unescapeLeadingUnderscores, @@ -53,6 +54,7 @@ import { type BigIntLiteralType, CheckFlags, type ConditionalType, + type ConfiguredProjectId, DiagnosticCategory, type DocumentIdentifier, EmitOnly, @@ -60,6 +62,7 @@ import { type ImportAdderAction, type IndexedAccessType, type IndexType, + type InferredProjectId, type InterfaceType, type IntrinsicType, isErrorType, @@ -67,11 +70,15 @@ import { ModifierFlags, ModuleKind, ObjectFlags, + type Program, + type Project, + type ProjectId, type Signature, SignatureKind, type Snapshot, type StringMappingType, SymbolFlags, + type SyntheticProjectId, type TemplateLiteralType, type TextEdit, TypeFlags, @@ -104,6 +111,30 @@ import { } from "./api.testUtils.ts"; describe("API", () => { + test("getCurrentLanguageServerSnapshot is LSP-only", () => { + if (!!false) { + const standalone = new API(); + // @ts-expect-error The standalone API has no canonical language server state. + void standalone.getCurrentLanguageServerSnapshot(); + + const lsp = undefined! as API; + void lsp.getCurrentLanguageServerSnapshot({ openProjects: ["/tsconfig.json"] }); + const baseSnapshot = undefined! as Snapshot; + void lsp.getCurrentLanguageServerSnapshot(undefined, baseSnapshot); + + const configured = undefined! as ConfiguredProjectId; + const inferred = undefined! as InferredProjectId; + const synthetic = undefined! as SyntheticProjectId; + const path: Path = configured; + const projectIds: ProjectId[] = [configured, inferred, synthetic]; + void path; + void projectIds; + // @ts-expect-error Project ID brands are not interchangeable. + const invalid: ConfiguredProjectId = synthetic; + void invalid; + } + }); + test("initializes once for concurrent first requests", async () => { await using api = spawnAPI(); // @sync-skip-block-start @@ -327,6 +358,34 @@ describe("API", () => { await assert.rejects(program.getSourceFileNames(), /snapshot .* not found/); // @sync: assert.throws(() => program.getSourceFileNames(), /snapshot .* not found/); }); + test("createSnapshot creates independent synthetic programs", async () => { + await using api = spawnAPI({ + "/src/a.ts": `export const a = 1;`, + "/src/b.ts": `export const b = 2;`, + }); + + const snapshot = await api.createSnapshot({ + createPrograms: [ + { rootFiles: ["/src/a.ts"], options: { compilerOptions: { noLib: true } } }, + { rootFiles: ["/src/b.ts"], options: { compilerOptions: { noLib: true, strict: true } } }, + ], + }); + assert.equal(snapshot.getProjects().length, 2); + assert.deepEqual(snapshot.getProjects().map(project => project.rootFiles), [["/src/a.ts"], ["/src/b.ts"]]); + assert.equal(snapshot.operation.createdPrograms!.length, 2); + for (const program of snapshot.operation.createdPrograms!) { + const syntheticProjectId: SyntheticProjectId = program.id; + void syntheticProjectId; + assert.strictEqual(snapshot.getProgram(program.id), program); + assert.strictEqual(snapshot.getProject(program.id), program.getProject()); + } + + const empty = await api.createSnapshot(); + assert.deepEqual(empty.getProjects(), []); + assert.equal("createdPrograms" in empty.operation, false); + assert.equal("openedFiles" in empty.operation, false); + }); + test("createProgram ignores an on-disk tsconfig", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ @@ -387,41 +446,6 @@ describe("API", () => { await program.dispose(); }); - test("createProgram updates roots when given an old program", async () => { - const options = { compilerOptions: { noLib: true } }; - await using api = spawnAPI({ - "/src/a.ts": `export const a = 1;`, - "/src/b.ts": `export const b = 1;`, - "/src/c.ts": `export const c = 1;`, - }); - - const oldProgram = await api.createProgram(["/src/a.ts", "/src/b.ts"], options); - const newProgram = await api.createProgram(["/src/a.ts", "/src/c.ts"], options, oldProgram); - assert.deepEqual(await newProgram.getSourceFileNames(), ["/src/a.ts", "/src/c.ts"]); - assert.deepEqual(await oldProgram.getSourceFileNames(), ["/src/a.ts", "/src/b.ts"]); - - await newProgram.dispose(); - await oldProgram.dispose(); - }); - - test("createProgram rejects an inactive or foreign old program", async () => { - const options = { compilerOptions: { noLib: true } }; - await using api = spawnAPI({ "/src/index.ts": `export const local = 1;` }); - await using otherAPI = spawnAPI({ "/src/index.ts": `export const foreign = 1;` }); - - const localProgram = await api.createProgram(["/src/index.ts"], options); - const foreignProgram = await otherAPI.createProgram(["/src/index.ts"], options); - - const createFromForeignProgram = () => api.createProgram(["/src/index.ts"], options, foreignProgram); - await assert.rejects(createFromForeignProgram, /oldProgram must belong to this API instance and reference an active snapshot/); // @sync: assert.throws(createFromForeignProgram, /oldProgram must belong to this API instance and reference an active snapshot/); - - await localProgram.dispose(); - const createFromDisposedProgram = () => api.createProgram(["/src/index.ts"], options, localProgram); - await assert.rejects(createFromDisposedProgram, /oldProgram must belong to this API instance and reference an active snapshot/); // @sync: assert.throws(createFromDisposedProgram, /oldProgram must belong to this API instance and reference an active snapshot/); - - await foreignProgram.dispose(); - }); - test("createProgram discovers imported non-root dependencies", async () => { await using api = spawnAPI({ "/src/main.ts": `import { dependency } from "./dependency"; export const value = dependency;`, @@ -434,95 +458,6 @@ describe("API", () => { await program.dispose(); }); - test("createProgram updates an old program with file changes", async () => { - const fileName = "/src/index.ts"; - const options = { compilerOptions: { noLib: true, strict: true } }; - const { api: disposableAPI, fs } = spawnAPIWithFS({ - [fileName]: `export const value: string = 1;`, - }); - await using api = disposableAPI; - - const oldProgram = await api.createProgram([fileName], options); - assert.equal((await oldProgram.getSemanticDiagnostics(fileName)).length, 1); - - fs.writeFile!(fileName, `export const value: string = "valid";`); - const newProgram = await api.createProgram( - [fileName], - options, - oldProgram, - { changed: [fileName] }, - ); - - assert.equal((await newProgram.getSemanticDiagnostics(fileName)).length, 0); - assert.equal((await oldProgram.getSemanticDiagnostics(fileName)).length, 1); - - await newProgram.dispose(); - await oldProgram.dispose(); - }); - - test("createProgram updates an old program with invalidateAll", async () => { - const fileName = "/src/index.ts"; - const options = { compilerOptions: { noLib: true, strict: true } }; - const { api: disposableAPI, fs } = spawnAPIWithFS({ - [fileName]: `export const value: string = 1;`, - }); - await using api = disposableAPI; - - const oldProgram = await api.createProgram([fileName], options); - assert.equal((await oldProgram.getSemanticDiagnostics(fileName)).length, 1); - - fs.writeFile!(fileName, `export const value: string = "valid";`); - const newProgram = await api.createProgram( - [fileName], - options, - oldProgram, - { invalidateAll: true }, - ); - - assert.equal((await newProgram.getSemanticDiagnostics(fileName)).length, 0); - assert.equal((await oldProgram.getSemanticDiagnostics(fileName)).length, 1); - - await newProgram.dispose(); - await oldProgram.dispose(); - }); - - test("createProgram accepts a regular project program as the old program", async () => { - const fileName = "/src/index.ts"; - const { api: disposableAPI, fs } = spawnAPIWithFS({ - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, strict: true } }), - [fileName]: `export const value: string = 1;`, - }); - await using api = disposableAPI; - - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; - assert.equal((await project.program.getSemanticDiagnostics(fileName)).length, 1); - - fs.writeFile!(fileName, `export const value: string = "valid";`); - const newProgram = await api.createProgram( - project.parsedCommandLine.fileNames, - { - compilerOptions: project.parsedCommandLine.options, - ...(project.parsedCommandLine.projectReferences - ? { projectReferences: project.parsedCommandLine.projectReferences } - : {}), - }, - project.program, - { changed: [fileName] }, - ); - - assert.equal((await newProgram.getSemanticDiagnostics(fileName)).length, 0); - assert.equal((await project.program.getSemanticDiagnostics(fileName)).length, 1); - await newProgram.dispose(); - }); - - test("createProgram rejects file changes without an old program", async () => { - await using api = spawnAPI({ "/src/index.ts": `export const value = 1;` }); - - const createWithChanges = () => api.createProgram(["/src/index.ts"], { compilerOptions: { noLib: true } }, undefined, { changed: ["/src/index.ts"] }); - await assert.rejects(createWithChanges, /fileChanges requires an oldProgram/); // @sync: assert.throws(createWithChanges, /fileChanges requires an oldProgram/); - }); - test("parseConfigFile", async () => { await using api = spawnAPI(); @@ -686,8 +621,8 @@ describe("API - batchContext", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("value:")); assert.ok(symbol); const type = await project.checker.getTypeOfSymbol(symbol); @@ -715,8 +650,8 @@ describe("Checker - getImmediateAliasedSymbol", () => { "/src/main.ts": `import { foo } from "./foo";\nexport const usage = foo;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = `import { foo } from "./foo";`.indexOf("foo }"); const aliasSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(aliasSymbol); @@ -743,8 +678,8 @@ test(); `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const nodes: Array = []; @@ -765,14 +700,14 @@ test(); }); describe("Snapshot", () => { - test("updateSnapshot returns snapshot with projects", async () => { + test("createSnapshot returns snapshot with projects", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); assert.ok(snapshot); assert.ok(snapshot.id); assert.ok(snapshot.getProjects().length > 0); - assert.ok(snapshot.getProject("/tsconfig.json")); + assert.ok(snapshot.getConfiguredProject("/tsconfig.json")); }); test("project exposes parsedCommandLine", async () => { @@ -781,8 +716,8 @@ describe("Snapshot", () => { "/tsconfig.json": JSON.stringify({ compileOnSave: true }), }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; assert.deepEqual(project.parsedCommandLine.fileNames, ["/src/index.ts", "/src/foo.ts"]); assert.deepEqual(project.parsedCommandLine.options, { configFilePath: "/tsconfig.json" }); assert.equal(project.parsedCommandLine.compileOnSave, true); @@ -793,8 +728,8 @@ describe("Snapshot", () => { test("getSymbolAtPosition", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); assert.equal(symbol.name, "foo"); @@ -804,8 +739,8 @@ describe("Snapshot", () => { test("getSymbolAtLocation", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const node = cast( @@ -822,8 +757,8 @@ describe("Snapshot", () => { test("getSymbolOfSourceFile", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const moduleSymbol = await project.checker.getSymbolOfSourceFile("/src/foo.ts"); assert.ok(moduleSymbol); const exports = await moduleSymbol.getExports(); @@ -836,8 +771,8 @@ describe("Snapshot", () => { "/src/script.ts": `const x = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolOfSourceFile("/src/script.ts"); assert.equal(symbol, undefined); }); @@ -845,8 +780,8 @@ describe("Snapshot", () => { test("getSymbolOfSourceFile batched", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbols = await project.checker.getSymbolOfSourceFile(["/src/index.ts", "/src/foo.ts"]); assert.equal(symbols.length, 2); assert.ok(symbols[0]); @@ -857,8 +792,8 @@ describe("Snapshot", () => { test("getTypeOfSymbol", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); const type = await project.checker.getTypeOfSymbol(symbol); @@ -884,8 +819,8 @@ describe("Snapshot", () => { }); // when `"exactOptionalPropertyTypes": true` - const snapshot1 = await api.updateSnapshot({ openProject: "/tsconfig-one.json" }); - const project1 = snapshot1.getProject("/tsconfig-one.json")!; + const snapshot1 = await api.createSnapshot({ openProject: "/tsconfig-one.json" }); + const project1 = snapshot1.getConfiguredProject("/tsconfig-one.json")!; const type1 = await project1.checker.getTypeAtPosition("/src/index.ts", 7); assert.ok(type1); const symbol1 = await project1.checker.getPropertyOfType(type1, "a"); @@ -901,8 +836,8 @@ describe("Snapshot", () => { assert.ok(propertyType2.flags & TypeFlags.String); // when `"exactOptionalPropertyTypes": false` - const snapshot2 = await api.updateSnapshot({ openProject: "/tsconfig-two.json" }); - const project2 = snapshot2.getProject("/tsconfig-two.json")!; + const snapshot2 = await api.createSnapshot({ openProject: "/tsconfig-two.json" }); + const project2 = snapshot2.getConfiguredProject("/tsconfig-two.json")!; const type2 = await project2.checker.getTypeAtPosition("/src/index.ts", 7); assert.ok(type2); const symbol2 = await project2.checker.getPropertyOfType(type2, "a"); @@ -927,8 +862,8 @@ describe("LanguageService - imports", () => { "/src/foo.ts": `export const foo = 1;\n`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/foo.ts", "export const ".length); assert.ok(symbol); @@ -945,8 +880,8 @@ describe("LanguageService - imports", () => { "/src/foo.ts": `export const foo = 1;\nexport const bar = 2;\n`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const foo = await project.checker.getSymbolAtPosition("/src/foo.ts", "export const ".length); const bar = await project.checker.getSymbolAtPosition("/src/foo.ts", "export const foo = 1;\nexport const ".length); assert.ok(foo); @@ -968,8 +903,8 @@ describe("LanguageService - imports", () => { "/src/foo.ts": `export const foo = 1;\nexport const bar = 2;\n`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const bar = await project.checker.getSymbolAtPosition("/src/foo.ts", "export const foo = 1;\nexport const ".length); assert.ok(bar); @@ -988,8 +923,8 @@ describe("LanguageService - imports", () => { "/src/foo.ts": `const local = 1;\n`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/foo.ts", "const ".length); assert.ok(symbol); @@ -1003,8 +938,8 @@ describe("LanguageService - imports", () => { test("getImportAdderEdits rejects invalid actions", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/foo.ts", 13); assert.ok(symbol); @@ -1027,8 +962,8 @@ describe("LanguageService - getCompletionsAtPosition", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // Position right after "obj." — member completion trigger const pos = src.indexOf("obj.") + "obj.".length; const completions = await project.languageService.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: "." }); @@ -1046,8 +981,8 @@ describe("LanguageService - getCompletionsAtPosition", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("obj.") + "obj.".length; const completions = await project.languageService.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: "." }); assert.ok(completions); @@ -1061,8 +996,8 @@ describe("LanguageService - getCompletionsAtPosition", () => { "/src/main.ts": `export {};`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const completions = await project.languageService.getCompletionsAtPosition("/src/does-not-exist.ts", 0); assert.equal(completions, undefined, "Expected undefined for non-existent file"); }); @@ -1074,8 +1009,8 @@ describe("LanguageService - getCompletionsAtPosition", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("obj.") + "obj.".length; const completions = await project.languageService.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: ".", includeSymbol: true }); assert.ok(completions, "Expected completions"); @@ -1093,8 +1028,8 @@ describe("LanguageService - getReferencedSymbolsForNode", () => { "/src/index.ts": `function greet(name: string) { return name; }\ngreet("world");`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const funcDecl = cast(sourceFile.statements[0], isFunctionDeclaration); @@ -1115,8 +1050,8 @@ describe("LanguageService - getSignatureUsage", () => { "/src/index.ts": `function greet(name: string) { return name; }\ngreet("world");`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const funcDecl = cast(sourceFile.statements[0], isFunctionDeclaration); @@ -1135,8 +1070,8 @@ describe("Checker - getApparentType", () => { "/src/main.ts": `export const x = "hello" as const;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = `export const x = "hello" as const;`.indexOf("x ="); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -1173,8 +1108,8 @@ class QuotaExceededError extends TaggedError("QuotaExceededError")<{ export type Result = RateLimitError | (RateLimitError & QuotaExceededError);`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const typeAlias = sourceFile.statements.find(isTypeAliasDeclaration); @@ -1195,8 +1130,8 @@ describe("Checker - getMemberInModuleExports", () => { "/src/index.ts": `export const direct = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile); @@ -1213,8 +1148,8 @@ describe("SourceFile", () => { test("getSourceFile rejects invalid document identifiers", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const document = { fileName: "/src/index.ts" } as unknown as DocumentIdentifier; await assert.rejects( // @sync: assert.throws( @@ -1240,8 +1175,8 @@ describe("SourceFile", () => { "/node_modules/my-lib/index.d.ts": `export declare const bar: number;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const fileNames = await project.program.getSourceFileNames(); assert.deepEqual(fileNames, [ "/src/foo.ts", @@ -1262,8 +1197,8 @@ describe("SourceFile", () => { "/node_modules/my-lib/index.d.ts": `export declare const bar: number;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const program = project.program; const index = await program.getSourceFile("/src/index.ts"); @@ -1299,8 +1234,8 @@ describe("SourceFile", () => { "/esm/index.ts": `export const m = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const mts = await program.getSourceFile("/src/esm.mts"); assert.ok(mts); @@ -1325,8 +1260,8 @@ describe("SourceFile", () => { test("file properties", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -1337,8 +1272,8 @@ describe("SourceFile", () => { test("extended data", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -1371,8 +1306,8 @@ describe("SourceFile", () => { "/input.ts": `let arrow = () => {}`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/input.ts"); assert.ok(sourceFile); @@ -1406,8 +1341,8 @@ describe("NodeArray", () => { "/src/main.ts": `declare function foo(...args: any): void;\nfoo("a", "b",);\nfoo("a", "b");`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const statements = sourceFile.statements.filter(isExpressionStatement); @@ -1426,8 +1361,8 @@ test("unicode escapes", async () => { "/src/3.ts": `"\\ud800a\\udc00"`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const expectedTexts = new Map([ ["/src/1.ts", "😃"], ["/src/2.ts", "😃"], @@ -1453,8 +1388,8 @@ test("template unicode escapes", async () => { "/src/index.ts": "`\\ud800${0}\\udc00`", }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -1478,8 +1413,8 @@ test("template unicode escapes", async () => { test("Object equality", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // Same symbol returned from same snapshot's checker assert.strictEqual( await project.checker.getSymbolAtPosition("/src/index.ts", 9), @@ -1490,8 +1425,8 @@ test("Object equality", async () => { test("Snapshot dispose", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); @@ -1502,7 +1437,7 @@ test("Snapshot dispose", async () => { // After dispose, snapshot methods should throw assert.throws(() => { - snapshot.getProject("/tsconfig.json"); + snapshot.getConfiguredProject("/tsconfig.json"); }, { name: "Error", message: "Snapshot is disposed", @@ -1513,12 +1448,12 @@ describe("Multiple snapshots", () => { test("two snapshots work independently", async () => { await using api = spawnAPI(); - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const snap2 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const snap2 = await api.createSnapshot({ openProject: "/tsconfig.json" }); // Both can fetch source files - const sf1 = await snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); - const sf2 = await snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf1 = await snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf2 = await snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf1); assert.ok(sf2); @@ -1528,7 +1463,7 @@ describe("Multiple snapshots", () => { assert.ok(!snap2.isDisposed()); // snap2 still works after snap1 is disposed - const symbol = await snap2.getProject("/tsconfig.json")!.checker.getSymbolAtPosition("/src/index.ts", 9); + const symbol = await snap2.getConfiguredProject("/tsconfig.json")!.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); assert.equal(symbol.name, "foo"); }); @@ -1537,21 +1472,22 @@ describe("Multiple snapshots", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); await using api = disposableAPI; - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); // Verify initial state - const sf1 = await snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf1 = await snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf1); assert.equal(sf1.text, `export const foo = 42;`); // Mutate the file and create a new snapshot with the change fs.writeFile!("/src/foo.ts", `export const foo = "changed";`); - const snap2 = await api.updateSnapshot({ + const snap2 = await api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/foo.ts"] }, }); // snap2 should reflect the updated content - const sf2 = await snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf2 = await snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf2); assert.equal(sf2.text, `export const foo = "changed";`); @@ -1561,14 +1497,14 @@ describe("Multiple snapshots", () => { await snap1.dispose(); // snap2 still works independently after snap1 is disposed - const symbol = await snap2.getProject("/tsconfig.json")!.checker.getSymbolAtPosition("/src/index.ts", 9); + const symbol = await snap2.getConfiguredProject("/tsconfig.json")!.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); await snap2.dispose(); // Both are disposed, new snapshot works fine with latest content - const snap3 = await api.updateSnapshot(); - const sf3 = await snap3.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const snap3 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const sf3 = await snap3.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf3); assert.equal(sf3.text, `export const foo = "changed";`); }); @@ -1577,20 +1513,21 @@ describe("Multiple snapshots", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); await using api = disposableAPI; - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); // Add a brand new file fs.writeFile!("/src/bar.ts", `export const bar = true;`); - const snap2 = await api.updateSnapshot({ + const snap2 = await api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { created: ["/src/bar.ts"] }, }); - const sf = await snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/bar.ts"); + const sf = await snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/bar.ts"); assert.ok(sf); assert.equal(sf.text, `export const bar = true;`); // Original snapshot shouldn't have the new file - const sfOld = await snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/bar.ts"); + const sfOld = await snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/bar.ts"); assert.equal(sfOld, undefined); }); @@ -1598,7 +1535,7 @@ describe("Multiple snapshots", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); await using api = disposableAPI; - await api.updateSnapshot({ openProject: "/tsconfig.json" }); + await api.createSnapshot({ openProject: "/tsconfig.json" }); const versions = [ `export const foo = 1;`, @@ -1608,22 +1545,104 @@ describe("Multiple snapshots", () => { for (const version of versions) { fs.writeFile!("/src/foo.ts", version); - const snap = await api.updateSnapshot({ + const snap = await api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/foo.ts"] }, }); - const sf = await snap.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf = await snap.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf); assert.equal(sf.text, version); } }); + + test("snapshot.update derives from its receiver and reconstructs unchanged projects", async () => { + const { api: disposableAPI, fs } = spawnAPIWithFS({ + "/first/tsconfig.json": `{}`, + "/first/index.ts": `export const first = 1;`, + "/second/tsconfig.json": `{}`, + "/second/index.ts": `export const second = 1;`, + }); + await using api = disposableAPI; + + const base = await api.createSnapshot({ + openProjects: ["/first/tsconfig.json", "/second/tsconfig.json"], + }); + const baseFirst = await base.getConfiguredProject("/first/tsconfig.json")!.program.getSourceFile("/first/index.ts"); + const baseSecondProject = base.getConfiguredProject("/second/tsconfig.json")!; + + fs.writeFile!("/first/index.ts", `export const first = 2;`); + const updated = await base.update({ + fileChanges: { changed: ["/first/index.ts"] }, + ensurePrograms: [base.getConfiguredProject("/first/tsconfig.json")!.id], + }); + + assert.equal(updated.getProjects().length, 2); + assert.notStrictEqual(updated.getConfiguredProject("/second/tsconfig.json"), baseSecondProject); + assert.equal((await updated.getConfiguredProject("/first/tsconfig.json")!.program.getSourceFile("/first/index.ts"))!.text, `export const first = 2;`); + assert.equal(baseFirst!.text, `export const first = 1;`); + }); + + test("snapshot.update ensures all dirty programs", async () => { + const { api: disposableAPI, fs } = spawnAPIWithFS({ + "/configured/tsconfig.json": `{}`, + "/configured/index.ts": `export const configured = 1;`, + "/inferred.ts": `export const inferred = 1;`, + "/synthetic.ts": `export const synthetic = 1;`, + }); + await using api = disposableAPI; + + const created = await api.createSnapshot({ + openProjects: ["/configured/tsconfig.json"], + openFiles: ["/inferred.ts", "/configured/index.ts"], + createPrograms: [{ + rootFiles: ["/synthetic.ts"], + options: { compilerOptions: { noLib: true } }, + }], + }); + assert.equal(created.getProjects().length, 3); + const configuredProjectId: ConfiguredProjectId = created.getConfiguredProject("/configured/tsconfig.json")!.id; + void configuredProjectId; + assert.strictEqual(created.operation.openedFiles![0].project, created.getProject(created.operation.openedFiles![0].project.id)); + assert.strictEqual(created.operation.openedFiles![1].project, created.getConfiguredProject("/configured/tsconfig.json")); + for (const project of created.getProjects()) { + assert.equal(project.dirty, false, `${project.id} should be ensured when opened or created`); + } + + fs.writeFile!("/configured/index.ts", `export const configured = 2;`); + fs.writeFile!("/inferred.ts", `export const inferred = 2;`); + fs.writeFile!("/synthetic.ts", `export const synthetic = 2;`); + const dirty = await created.update({ + fileChanges: { changed: ["/configured/index.ts", "/inferred.ts", "/synthetic.ts"] }, + }); + assert.deepEqual(dirty.getProjects().map(project => project.dirty), [true, true, true]); + + const ensured = await dirty.update({ ensurePrograms: true }); + assert.deepEqual(ensured.getProjects().map(project => project.dirty), [false, false, false]); + + const withOperationResults = await ensured.update({ + openFiles: ["/inferred.ts"], + createPrograms: [{ + rootFiles: ["/synthetic.ts"], + options: { compilerOptions: { noLib: true } }, + }], + }); + const openedProject: Project = withOperationResults.operation.openedFiles[0].project; + const createdProgram: Program = withOperationResults.operation.createdPrograms[0]; + const openedFilesTuple: readonly [{ readonly project: Project; }] = withOperationResults.operation.openedFiles; + const createdProgramsTuple: readonly [Program] = withOperationResults.operation.createdPrograms; + void openedFilesTuple; + void createdProgramsTuple; + assert.strictEqual(withOperationResults.getProject(openedProject.id), openedProject); + assert.strictEqual(withOperationResults.getProgram(createdProgram.id), createdProgram); + }); }); describe("Source file caching", () => { test("same file from same snapshot returns cached object", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sf1 = await project.program.getSourceFile("/src/index.ts"); const sf2 = await project.program.getSourceFile("/src/index.ts"); assert.ok(sf1); @@ -1633,11 +1652,11 @@ describe("Source file caching", () => { test("same file from two snapshots (same content) returns cached object", async () => { await using api = spawnAPI(); - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const snap2 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const snap2 = await api.createSnapshot({ openProject: "/tsconfig.json" }); // Fetch from snap1 first (populates cache), then snap2 (cache hit via hash) - const sf1 = await snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); - const sf2 = await snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf1 = await snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf2 = await snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf1); assert.ok(sf2); // Same content hash → cache hit → same object @@ -1648,8 +1667,8 @@ describe("Source file caching", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); await using api = disposableAPI; - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const sf1 = await snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const sf1 = await snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf1); assert.equal(sf1.text, `export const foo = 42;`); @@ -1657,10 +1676,11 @@ describe("Source file caching", () => { fs.writeFile!("/src/foo.ts", `export const foo = 100;`); // Notify the server about the change - const snap2 = await api.updateSnapshot({ + const snap2 = await api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/foo.ts"] }, }); - const sf2 = await snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf2 = await snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf2); assert.equal(sf2.text, `export const foo = 100;`); @@ -1672,50 +1692,48 @@ describe("Source file caching", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); await using api = disposableAPI; - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const sf1 = await snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const sf1 = await snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf1); // Mutate a different file fs.writeFile!("/src/foo.ts", `export const foo = 999;`); // Notify the server about the change to foo.ts only - const snap2 = await api.updateSnapshot({ + const snap2 = await api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/foo.ts"] }, }); - const sf2 = await snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf2 = await snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf2); // index.ts wasn't changed — should still get cached object assert.strictEqual(sf1, sf2, "Unchanged file should return cached object across snapshots"); }); - test("cache entries survive when one of two snapshots is disposed", async () => { + test("disposing the source snapshot releases its cache entries", async () => { await using api = spawnAPI(); - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); // Fetch from snap1 to populate cache - const sf1 = await snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf1 = await snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf1); - // snap2 retains snap1's cache refs for unchanged files via snapshot changes - const snap2 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap2 = await api.createSnapshot({ openProject: "/tsconfig.json" }); - // Dispose snap1 — snap2 still holds a ref, so the entry survives await snap1.dispose(); - // Fetching from snap2 should still return the cached object - const sf2 = await snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf2 = await snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf2); - assert.strictEqual(sf1, sf2, "Cache entry should survive when retained by the next snapshot"); + assert.notStrictEqual(sf1, sf2, "independent snapshots do not implicitly retain each other's cache entries"); }); test("invalidateAll causes all files to be re-fetched", async () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); await using api = disposableAPI; - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const sf1 = await snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const sf1 = await snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf1); assert.equal(sf1.text, `export const foo = 42;`); @@ -1723,10 +1741,11 @@ describe("Source file caching", () => { fs.writeFile!("/src/foo.ts", `export const foo = "hello";`); // Use invalidateAll to force re-fetch - const snap2 = await api.updateSnapshot({ + const snap2 = await api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { invalidateAll: true }, }); - const sf2 = await snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf2 = await snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf2); assert.equal(sf2.text, `export const foo = "hello";`); assert.notStrictEqual(sf1, sf2, "invalidateAll should produce new source file objects"); @@ -1741,8 +1760,8 @@ describe("Source file caching", () => { await using api = disposableAPI; // Snapshot 1: get a node and verify getContextualType works - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const proj1 = snap1.getProject("/tsconfig.json")!; + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const proj1 = snap1.getConfiguredProject("/tsconfig.json")!; const sf1 = await proj1.program.getSourceFile("/src/main.ts"); assert.ok(sf1); @@ -1760,15 +1779,16 @@ describe("Source file caching", () => { // Snapshot 2: change a different file fs.writeFile!("/src/other.ts", `export const x = 2;`); - const snap2 = await api.updateSnapshot({ + const snap2 = await api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/other.ts"] }, }); - const proj2 = snap2.getProject("/tsconfig.json")!; + const proj2 = snap2.getConfiguredProject("/tsconfig.json")!; - // main.ts is unchanged — client returns the cached SourceFile (same object) + // Active snapshots may share a content-addressed source file. const sf2 = await proj2.program.getSourceFile("/src/main.ts"); assert.ok(sf2); - assert.strictEqual(sf1, sf2, "unchanged file should be served from client cache"); + assert.strictEqual(sf1, sf2); let numLiteral2: Expression | undefined; sf2.forEachChild(function visit(node) { @@ -1776,10 +1796,10 @@ describe("Source file caching", () => { node.forEachChild(visit); }); assert.ok(numLiteral2, "should find the 42 argument"); - assert.strictEqual(numLiteral, numLiteral2, "unchanged file should be served from client cache"); + assert.strictEqual(numLiteral, numLiteral2); // A type from new snapshot should be resolved - const type2 = await proj2.checker.getContextualType(numLiteral); + const type2 = await proj2.checker.getContextualType(numLiteral2); assert.ok(type2); assert.ok(type2.flags & TypeFlags.Number); }); @@ -1789,7 +1809,7 @@ describe("Snapshot disposal", () => { test("dispose is idempotent", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); const firstDispose = snapshot.dispose(); const secondDispose = snapshot.dispose(); assert.strictEqual(firstDispose, secondDispose); @@ -1804,7 +1824,7 @@ describe("Snapshot disposal", () => { const api = spawnAPI(); let snapshot: Snapshot; { - using disposableSnapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + using disposableSnapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); snapshot = disposableSnapshot; } assert.ok(snapshot.isDisposed()); @@ -1814,8 +1834,8 @@ describe("Snapshot disposal", () => { test("api.close disposes all active snapshots", async () => { const api = spawnAPI(); - const snap1 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const snap2 = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const snap2 = await api.createSnapshot({ openProject: "/tsconfig.json" }); assert.ok(!snap1.isDisposed()); assert.ok(!snap2.isDisposed()); await api.close(); @@ -1875,14 +1895,13 @@ describe("Source file cache keying across projects", () => { test("different parse modes produce separate cached objects; same parse modes share", async () => { await using api = spawnAPI(multiProjectFiles); - // Open all three projects - await api.updateSnapshot({ openProject: "/projectA/tsconfig.json" }); - await api.updateSnapshot({ openProject: "/projectB/tsconfig.json" }); - const snapshot = await api.updateSnapshot({ openProject: "/projectC/tsconfig.json" }); + const snapshot = await api.createSnapshot({ + openProjects: ["/projectA/tsconfig.json", "/projectB/tsconfig.json", "/projectC/tsconfig.json"], + }); - const projectA = snapshot.getProject("/projectA/tsconfig.json")!; - const projectB = snapshot.getProject("/projectB/tsconfig.json")!; - const projectC = snapshot.getProject("/projectC/tsconfig.json")!; + const projectA = snapshot.getConfiguredProject("/projectA/tsconfig.json")!; + const projectB = snapshot.getConfiguredProject("/projectB/tsconfig.json")!; + const projectC = snapshot.getConfiguredProject("/projectC/tsconfig.json")!; assert.ok(projectA, "projectA should exist"); assert.ok(projectB, "projectB should exist"); assert.ok(projectC, "projectC should exist"); @@ -1914,11 +1933,12 @@ describe("Checker - symbol identity across projects", () => { test("getSymbolAtPosition returns same Symbol instance across projects", async () => { await using api = spawnAPI(sharedSymbolFiles); - await api.updateSnapshot({ openProject: "/projectA/tsconfig.json" }); - const snapshot = await api.updateSnapshot({ openProject: "/projectB/tsconfig.json" }); + const snapshot = await api.createSnapshot({ + openProjects: ["/projectA/tsconfig.json", "/projectB/tsconfig.json"], + }); - const projectA = snapshot.getProject("/projectA/tsconfig.json")!; - const projectB = snapshot.getProject("/projectB/tsconfig.json")!; + const projectA = snapshot.getConfiguredProject("/projectA/tsconfig.json")!; + const projectB = snapshot.getConfiguredProject("/projectB/tsconfig.json")!; assert.ok(projectA, "projectA should exist"); assert.ok(projectB, "projectB should exist"); @@ -1953,8 +1973,8 @@ export class MyClass { test("getTypeAtPosition", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const xPos = src.indexOf("x = 42"); const type = await project.checker.getTypeAtPosition("/src/main.ts", xPos); @@ -1965,8 +1985,8 @@ export class MyClass { test("getTypeAtPosition batched", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const xPos = src.indexOf("x = 42"); const addPos = src.indexOf("add("); @@ -1979,8 +1999,8 @@ export class MyClass { test("getTypeAtLocation", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const firstVarDecl = sourceFile.statements[2]; // "export const x" @@ -2006,8 +2026,8 @@ const c = obj.b.c; await using api = spawnAPI(files); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -2052,8 +2072,8 @@ export class Cache { await using api = spawnAPI(files); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -2089,8 +2109,8 @@ export class Cache { test("getSignaturesOfType - call signatures", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const addPos = src.indexOf("add("); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", addPos); @@ -2114,8 +2134,8 @@ export class Cache { test("getApparentProperties includes CallableFunction members", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2133,8 +2153,8 @@ export class Cache { collectTiming: true, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2178,8 +2198,8 @@ export class Cache { test("getSignaturesOfType - construct signatures", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const classPos = src.indexOf("MyClass"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", classPos); @@ -2195,8 +2215,8 @@ export class Cache { test("Signature declaration can be resolved", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const addPos = src.indexOf("add("); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", addPos); @@ -2246,8 +2266,8 @@ export class Cache { "/src/main.ts": mainFile, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const liftPos = mainFile.indexOf("lift"); const type = await project.checker.getTypeAtPosition("/src/main.ts", liftPos); assert.ok(type); @@ -2266,8 +2286,8 @@ export class Cache { test("Signature.getParameters() returns parameter symbols with correct names", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2286,8 +2306,8 @@ export class Cache { test("Signature.getThisParameter() returns undefined when no explicit this parameter", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2306,8 +2326,8 @@ export class Cache { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("foo(")); assert.ok(symbol); const type = await project.checker.getTypeOfSymbol(symbol); @@ -2323,8 +2343,8 @@ export class Cache { test("Signature.getTarget() returns undefined for a non-instantiated signature", async () => { await using api = spawnAPI(checkerFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2346,8 +2366,8 @@ export class Cache { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let callNode: Node | undefined; @@ -2380,8 +2400,8 @@ export const value = 1; test("getMembers returns class members", async () => { await using api = spawnAPI(symbolFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = symbolFiles["/src/mod.ts"]; const animalPos = src.indexOf("Animal"); const symbol = await project.checker.getSymbolAtPosition("/src/mod.ts", animalPos); @@ -2396,8 +2416,8 @@ export const value = 1; test("getExports returns module exports via sourceFile symbol", async () => { await using api = spawnAPI(symbolFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/mod.ts"); assert.ok(sourceFile); const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile); @@ -2412,8 +2432,8 @@ export const value = 1; test("getParent returns containing symbol", async () => { await using api = spawnAPI(symbolFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = symbolFiles["/src/mod.ts"]; const namePos = src.indexOf("name:"); const nameSymbol = await project.checker.getSymbolAtPosition("/src/mod.ts", namePos); @@ -2427,8 +2447,8 @@ export const value = 1; test("checkFlags is typed as CheckFlags", async () => { await using api = spawnAPI(symbolFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/mod.ts", symbolFiles["/src/mod.ts"].indexOf("Animal")); assert.ok(symbol); const checkFlags: CheckFlags = symbol.checkFlags; @@ -2448,8 +2468,8 @@ export const instance: Foo = new Foo(); `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport class Foo {\n x: number = 0;\n}\nexport const instance: Foo = new Foo();\n`; const instancePos = src.indexOf("instance"); const symbol = await project.checker.getSymbolAtPosition("/src/types.ts", instancePos); @@ -2479,8 +2499,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; }; async function getTypeAtName(api: API, name: string) { - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = typeFiles["/src/types.ts"]; const pos = src.indexOf(name); assert.ok(pos >= 0, `Could not find "${name}" in source`); @@ -2543,8 +2563,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // `string` is neither a union/intersection nor a template literal type, // so it has no constituent types. The client guards on the type's flags @@ -2566,8 +2586,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("IndexType.getTarget() returns the target type", async () => { await using api = spawnAPI(typeFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.resolveName("KeyOf", SymbolFlags.TypeAlias, { document: "/src/types.ts", position: 0 }); assert.ok(symbol); const type = await project.checker.getDeclaredTypeOfSymbol(symbol); @@ -2582,8 +2602,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("IndexedAccessType.getObjectType() and getIndexType()", async () => { await using api = spawnAPI(typeFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.resolveName("Lookup", SymbolFlags.TypeAlias, { document: "/src/types.ts", position: 0 }); assert.ok(symbol); const type = await project.checker.getDeclaredTypeOfSymbol(symbol); @@ -2600,8 +2620,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("ConditionalType.getCheckType() and getExtendsType()", async () => { await using api = spawnAPI(typeFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.resolveName("Cond", SymbolFlags.TypeAlias, { document: "/src/types.ts", position: 0 }); assert.ok(symbol); const type = await project.checker.getDeclaredTypeOfSymbol(symbol); @@ -2618,8 +2638,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("ConditionalType.getTrueType() and getFalseType()", async () => { await using api = spawnAPI(typeFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.resolveName("Cond", SymbolFlags.TypeAlias, { document: "/src/types.ts", position: 0 }); assert.ok(symbol); const type = await project.checker.getDeclaredTypeOfSymbol(symbol); @@ -2653,8 +2673,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("StringMappingType.getTarget() returns the mapped type", async () => { await using api = spawnAPI(typeFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = typeFiles["/src/types.ts"]; const pos = src.indexOf("Upper"); const symbol = await project.checker.getSymbolAtPosition("/src/types.ts", pos); @@ -2683,8 +2703,8 @@ array([]); `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -2735,8 +2755,8 @@ export function gh1449(a: T): T { `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const functionDeclaration = sourceFile.statements.find(isFunctionDeclaration); @@ -2773,8 +2793,8 @@ describe("Checker - intrinsic type getters", () => { test("getAnyType returns a type with Any flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getAnyType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Any); @@ -2783,8 +2803,8 @@ describe("Checker - intrinsic type getters", () => { test("getStringType returns a type with String flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getStringType(); assert.ok(type); assert.ok(type.flags & TypeFlags.String); @@ -2793,8 +2813,8 @@ describe("Checker - intrinsic type getters", () => { test("getNumberType returns a type with Number flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getNumberType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Number); @@ -2803,8 +2823,8 @@ describe("Checker - intrinsic type getters", () => { test("getBooleanType returns a type with Boolean flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getBooleanType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Boolean); @@ -2813,8 +2833,8 @@ describe("Checker - intrinsic type getters", () => { test("getVoidType returns a type with Void flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getVoidType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Void); @@ -2823,8 +2843,8 @@ describe("Checker - intrinsic type getters", () => { test("getUndefinedType returns a type with Undefined flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getUndefinedType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Undefined); @@ -2833,8 +2853,8 @@ describe("Checker - intrinsic type getters", () => { test("getNullType returns a type with Null flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getNullType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Null); @@ -2843,8 +2863,8 @@ describe("Checker - intrinsic type getters", () => { test("getNeverType returns a type with Never flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getNeverType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Never); @@ -2853,8 +2873,8 @@ describe("Checker - intrinsic type getters", () => { test("getUnknownType returns a type with Unknown flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getUnknownType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Unknown); @@ -2863,8 +2883,8 @@ describe("Checker - intrinsic type getters", () => { test("getBigIntType returns a type with BigInt flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getBigIntType(); assert.ok(type); assert.ok(type.flags & TypeFlags.BigInt); @@ -2873,8 +2893,8 @@ describe("Checker - intrinsic type getters", () => { test("getESSymbolType returns a type with ESSymbol flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getESSymbolType(); assert.ok(type); assert.ok(type.flags & TypeFlags.ESSymbol); @@ -2883,8 +2903,8 @@ describe("Checker - intrinsic type getters", () => { test("getNonPrimitiveType returns a type with NonPrimitive flag", async () => { await using api = spawnAPI(intrinsicFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = await project.checker.getNonPrimitiveType(); assert.ok(type); assert.ok(type.flags & TypeFlags.NonPrimitive); @@ -2902,14 +2922,13 @@ describe("Checker - multi-project type ID uniqueness", () => { "/proj3/src/index.ts": `export const z = true;`, }); - // Open all 3 projects — each updateSnapshot accumulates open projects - await api.updateSnapshot({ openProject: "/proj1/tsconfig.json" }); - await api.updateSnapshot({ openProject: "/proj2/tsconfig.json" }); - const snapshot = await api.updateSnapshot({ openProject: "/proj3/tsconfig.json" }); + const snapshot = await api.createSnapshot({ + openProjects: ["/proj1/tsconfig.json", "/proj2/tsconfig.json", "/proj3/tsconfig.json"], + }); - const proj1 = snapshot.getProject("/proj1/tsconfig.json")!; - const proj2 = snapshot.getProject("/proj2/tsconfig.json")!; - const proj3 = snapshot.getProject("/proj3/tsconfig.json")!; + const proj1 = snapshot.getConfiguredProject("/proj1/tsconfig.json")!; + const proj2 = snapshot.getConfiguredProject("/proj2/tsconfig.json")!; + const proj3 = snapshot.getConfiguredProject("/proj3/tsconfig.json")!; assert.ok(proj1, "proj1 should be in final snapshot"); assert.ok(proj2, "proj2 should be in final snapshot"); assert.ok(proj3, "proj3 should be in final snapshot"); @@ -2955,13 +2974,13 @@ describe("Checker - multi-project type ID uniqueness", () => { "/proj3/src/index.ts": `export function toggle(b: boolean): boolean { return !b; }`, }); - await api.updateSnapshot({ openProject: "/proj1/tsconfig.json" }); - await api.updateSnapshot({ openProject: "/proj2/tsconfig.json" }); - const snapshot = await api.updateSnapshot({ openProject: "/proj3/tsconfig.json" }); + const snapshot = await api.createSnapshot({ + openProjects: ["/proj1/tsconfig.json", "/proj2/tsconfig.json", "/proj3/tsconfig.json"], + }); - const proj1 = snapshot.getProject("/proj1/tsconfig.json")!; - const proj2 = snapshot.getProject("/proj2/tsconfig.json")!; - const proj3 = snapshot.getProject("/proj3/tsconfig.json")!; + const proj1 = snapshot.getConfiguredProject("/proj1/tsconfig.json")!; + const proj2 = snapshot.getConfiguredProject("/proj2/tsconfig.json")!; + const proj3 = snapshot.getConfiguredProject("/proj3/tsconfig.json")!; // Get a symbol from each project (exercises symbol registry) const src1 = `export function add(a: number, b: number): number { return a + b; }`; @@ -3005,8 +3024,8 @@ describe("Checker - getBaseTypeOfLiteralType", () => { "/src/main.ts": `export const x = 42;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const x = 42;`; const pos = src.indexOf("x ="); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3025,8 +3044,8 @@ describe("Checker - getBaseTypeOfLiteralType", () => { "/src/main.ts": `export const s = "hello";`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const s = "hello";`; const pos = src.indexOf("s "); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3050,8 +3069,8 @@ foo(42); `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -3089,8 +3108,8 @@ export function check(x: string | number) { `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport function check(x: string | number) {\n if (typeof x === "string") {\n return x;\n }\n return x;\n}\n`; // Get the symbol for parameter "x" @@ -3140,8 +3159,8 @@ export const obj = { name }; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -3189,8 +3208,8 @@ describe("readFile callback semantics", () => { fs, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // 1. String content: virtual file is found const sf = await project.program.getSourceFile("/src/index.ts"); @@ -3334,7 +3353,7 @@ describe("updateSnapshot file systems", () => { fs, }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: { kind: "full", @@ -3348,7 +3367,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.equal(sourceFile?.text, `export const source = "memory";`); assert.equal(await project.program.getSourceFile("/host.ts"), undefined); @@ -3359,14 +3378,14 @@ describe("updateSnapshot file systems", () => { await using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystemWithLib(Object.entries({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true }, files: ["src/main.ts"] }), "/src/main.ts": `export const values: Array = [];`, })), }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; assert.deepEqual(await program.getGlobalDiagnostics(), []); const sourceFileNames = await program.getSourceFileNames(); const defaultLibraryName = sourceFileNames.find(fileName => fileName.includes("/lib.") && fileName.endsWith(".d.ts")); @@ -3383,7 +3402,7 @@ describe("updateSnapshot file systems", () => { await using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openFiles: [fileDocument, remoteDocument, notebookDocument], fileSystem: createFileSystem([ [fileDocument, `export const file = true;`], @@ -3421,7 +3440,7 @@ describe("updateSnapshot file systems", () => { fs, }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: { kind: "layer", @@ -3435,7 +3454,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/tsconfig.json")!; assert.equal((await project.program.getSourceFile("/src/index.ts"))?.text, `export const cached = true;`); assert.equal((await project.program.getSourceFile("/src/fallback.ts"))?.text, `export const fallback = true;`); @@ -3454,14 +3473,14 @@ describe("updateSnapshot file systems", () => { }), }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystemLayer([ ["/tsconfig.json", JSON.stringify({ compilerOptions: { noLib: true }, include: ["src/**/*.ts"] })], ["/src/from-cache.ts", `export const cache = true;`], ]), }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; assert.deepEqual( [...await program.getSourceFileNames()].sort(), ["/src/from-cache.ts", "/src/from-host.ts"], @@ -3480,7 +3499,7 @@ describe("updateSnapshot file systems", () => { }, }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/project/tsconfig.json", fileSystem: { kind: "full", @@ -3494,7 +3513,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/project/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/project/tsconfig.json")!; assert.equal( (await project.program.getSourceFile("/packages/pkg/index.d.ts"))?.text, `export declare const value: number;`, @@ -3514,7 +3533,7 @@ describe("updateSnapshot file systems", () => { }, }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/project/tsconfig.json", fileSystem: { kind: "full", @@ -3528,7 +3547,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/project/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/project/tsconfig.json")!; assert.equal( (await project.program.getSourceFile("/project/pkg/index.d.ts"))?.text, `export declare const value: number;`, @@ -3540,7 +3559,7 @@ describe("updateSnapshot file systems", () => { await using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystem(Object.entries({ "/tsconfig.json": JSON.stringify({ @@ -3555,6 +3574,7 @@ describe("updateSnapshot file systems", () => { }); using updated = await snapshot.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer( Object.entries({ "/src/change.ts": `export const version = "new";`, @@ -3565,15 +3585,17 @@ describe("updateSnapshot file systems", () => { }, ), }); - const project = updated.getProject("/tsconfig.json")!; + const project = updated.getConfiguredProject("/tsconfig.json")!; assert.equal((await project.program.getSourceFile("/src/keep.ts"))?.text, `export const keep = true;`); assert.equal((await project.program.getSourceFile("/src/change.ts"))?.text, `export const version = "new";`); assert.equal((await project.program.getSourceFile("/src/added.ts"))?.text, `export const added = true;`); assert.equal(await project.program.getSourceFile("/src/remove.ts"), undefined); assert.equal(await project.program.getSourceFile("/src/removed/gone.ts"), undefined); - await assert.rejects(() => snapshot.update(), /can only update the latest snapshot/); // @sync: assert.throws(() => snapshot.update(), /can only update the latest snapshot/); + using fork = await snapshot.update({ ensurePrograms: true }); + assert.equal((await fork.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/change.ts"))?.text, `export const version = "old";`); using updatedAgain = await updated.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer( Object.entries({ "/src/added.ts": `export const added = "updated again";`, @@ -3583,7 +3605,7 @@ describe("updateSnapshot file systems", () => { }, ), }); - const updatedAgainProject = updatedAgain.getProject("/tsconfig.json")!; + const updatedAgainProject = updatedAgain.getConfiguredProject("/tsconfig.json")!; assert.equal((await updatedAgainProject.program.getSourceFile("/src/keep.ts"))?.text, `export const keep = true;`); assert.equal((await updatedAgainProject.program.getSourceFile("/src/added.ts"))?.text, `export const added = "updated again";`); assert.equal(await updatedAgainProject.program.getSourceFile("/src/change.ts"), undefined); @@ -3593,7 +3615,7 @@ describe("updateSnapshot file systems", () => { await using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - let snapshot: Snapshot = await api.updateSnapshot({ + let snapshot: Snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystem([ ["/tsconfig.json", JSON.stringify({ compilerOptions: { noLib: true }, files: ["pkg/index.ts"] })], @@ -3606,13 +3628,14 @@ describe("updateSnapshot file systems", () => { const oldSnapshot: Snapshot = snapshot; content += character; snapshot = await oldSnapshot.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer([["/pkg/index.ts", content]]), }); await oldSnapshot.dispose(); assert.equal(oldSnapshot.isDisposed(), true); } - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; assert.equal((await program.getSourceFile("/pkg/index.ts"))?.text, "export const x = 1"); } finally { @@ -3628,15 +3651,16 @@ describe("updateSnapshot file systems", () => { cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), fs: host, }); - using snapshot = await api.updateSnapshot(); + using snapshot = await api.createSnapshot(); using replaced = await snapshot.update({ + ensurePrograms: true, openProject: "/tsconfig.json", fileSystem: createFileSystem([ ["/tsconfig.json", JSON.stringify({ compilerOptions: { noLib: true }, files: ["memory.ts", "host.ts"] })], ["/memory.ts", `export const source = "memory";`], ]), }); - const program = replaced.getProject("/tsconfig.json")!.program; + const program = replaced.getConfiguredProject("/tsconfig.json")!.program; assert.equal((await program.getSourceFile("/memory.ts"))?.text, `export const source = "memory";`); assert.equal(await program.getSourceFile("/host.ts"), undefined); }); @@ -3645,7 +3669,7 @@ describe("updateSnapshot file systems", () => { await using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystem( Object.entries({ @@ -3663,6 +3687,7 @@ describe("updateSnapshot file systems", () => { }); using updated = await snapshot.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer( Object.entries({ "/target/change.ts": `export const version = "new";`, @@ -3673,7 +3698,7 @@ describe("updateSnapshot file systems", () => { }, ), }); - const program = updated.getProject("/tsconfig.json")!.program; + const program = updated.getConfiguredProject("/tsconfig.json")!.program; assert.equal((await program.getSourceFile("/src/link/change.ts"))?.text, `export const version = "new";`); assert.equal((await program.getSourceFile("/src/link/added.ts"))?.text, `export const added = true;`); assert.equal(await program.getSourceFile("/src/link/remove.ts"), undefined); @@ -3689,14 +3714,14 @@ describe("updateSnapshot file systems", () => { }, }, }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystem(Object.entries({ "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, outDir: "/out", rootDir: "/src" }, files: ["src/main.ts"] }), "/src/main.ts": `export const value: number = 1;`, })), }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const result = await program.emit(); assert.deepEqual(result.emittedFiles, ["/out/main.js"]); assert.deepEqual(result.fileSystem, { @@ -3709,7 +3734,7 @@ describe("updateSnapshot file systems", () => { using updated = await snapshot.update({ fileSystem: result.fileSystem!, openFiles: ["/out/main.js"] }); const outputProject = await updated.getDefaultProjectForFile("/out/main.js"); - assert.equal((await updated.getProject("/tsconfig.json")!.program.getSourceFile("/src/main.ts"))?.text, `export const value: number = 1;`); + assert.equal((await updated.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/main.ts"))?.text, `export const value: number = 1;`); assert.equal((await outputProject?.program.getSourceFile("/out/main.js"))?.text, `export const value = 1;\n`); }); @@ -3719,14 +3744,14 @@ describe("updateSnapshot file systems", () => { cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), fs: host, }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystemLayer(Object.entries({ "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, outDir: "/out", rootDir: "/src" }, files: ["src/main.ts"] }), "/src/main.ts": `export const value: number = 1;`, })), }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const result = await program.emit(); assert.equal(result.fileSystem, undefined); assert.equal(host.readFile!("/out/main.js"), `export const value = 1;\n`); @@ -3759,7 +3784,7 @@ describe("updateSnapshot file systems", () => { }, }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/project/tsconfig.json", fileSystem: { kind: "full", @@ -3772,7 +3797,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/project/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/project/tsconfig.json")!; const sourceFileNames = await project.program.getSourceFileNames(); assert.ok( sourceFileNames.includes("/host/node_modules/pkg/index.d.ts"), @@ -3794,7 +3819,7 @@ describe("updateSnapshot file systems", () => { cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), fs: host, }); - using snapshot = await api.updateSnapshot({ + using snapshot = await api.createSnapshot({ openProject: "/project/tsconfig.json", fileSystem: createFileSystem([ ["/project/tsconfig.json", JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node" }, files: ["index.ts"] })], @@ -3802,13 +3827,14 @@ describe("updateSnapshot file systems", () => { ]), }); using updated = await snapshot.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer([], { symlinks: { "/project/node_modules": { target: "/host/node_modules", host: true }, }, }), }); - const project = updated.getProject("/project/tsconfig.json")!; + const project = updated.getConfiguredProject("/project/tsconfig.json")!; assert.equal( (await project.program.getSourceFile("/host/node_modules/pkg/index.d.ts"))?.text, `export declare const value: string;`, @@ -3829,8 +3855,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const xs: number[] = [];`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const xs: number[] = [];`; const pos = src.indexOf("xs"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3847,8 +3873,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const xs: readonly number[] = [];`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const xs: readonly number[] = [];`; const pos = src.indexOf("xs"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3865,8 +3891,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const xs: Array = [];`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const xs: Array = [];`; const pos = src.indexOf("xs"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3883,8 +3909,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const tup: [number, string] = [1, "a"];`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const tup: [number, string] = [1, "a"];`; const pos = src.indexOf("tup"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3901,8 +3927,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const tup: readonly [number, string] = [1, "a"];`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const tup: readonly [number, string] = [1, "a"];`; const pos = src.indexOf("tup"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3919,8 +3945,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const str: string = "";`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const str: string = "";`; const pos = src.indexOf("str"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3946,8 +3972,8 @@ export type ReadonlyUser = Readonly; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const user = sourceFile.statements.find(isInterfaceDeclaration); @@ -3972,8 +3998,8 @@ export type ReadonlyUser = Readonly; "/src/main.ts": `export const a = 1; export let b = 2;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker } = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getConfiguredProject("/tsconfig.json")!; const a = await checker.getSymbolAtPosition("/src/main.ts", "export const ".length); const b = await checker.getSymbolAtPosition("/src/main.ts", "export const a = 1; export let ".length); assert.ok(a); @@ -4006,8 +4032,8 @@ export type B = InstanceType; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const typeAliases = sourceFile.statements.filter(isTypeAliasDeclaration); @@ -4030,8 +4056,8 @@ describe("Checker - getReturnTypeOfSignature", () => { "/src/main.ts": `export function add(a: number, b: number): number { return a + b; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function add(a: number, b: number): number { return a + b; }`; const pos = src.indexOf("add("); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4053,8 +4079,8 @@ describe("Checker - getRestTypeOfSignature", () => { "/src/main.ts": `export function sum(...nums: number[]): number { return nums.reduce((a, b) => a + b, 0); }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function sum(...nums: number[]): number { return nums.reduce((a, b) => a + b, 0); }`; const pos = src.indexOf("sum("); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4076,8 +4102,8 @@ describe("Checker - getTypePredicateOfSignature", () => { "/src/main.ts": `export function isString(x: unknown): x is string { return typeof x === "string"; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function isString(x: unknown): x is string { return typeof x === "string"; }`; const pos = src.indexOf("isString("); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4108,8 +4134,8 @@ export class Dog extends Animal { `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport class Animal {\n isdog(): this is Dog { return this instanceof Dog; }\n}\nexport class Dog extends Animal {\n bark() {}\n}\n`; const pos = src.indexOf("isdog("); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4129,8 +4155,8 @@ export class Dog extends Animal { "/src/main.ts": `export function assertIsString(x: unknown): asserts x is string { if (typeof x !== "string") throw new Error(); }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function assertIsString(x: unknown): asserts x is string { if (typeof x !== "string") throw new Error(); }`; const pos = src.indexOf("assertIsString("); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4154,8 +4180,8 @@ export class Dog extends Animal { "/src/main.ts": `export function add(a: number, b: number): number { return a + b; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function add(a: number, b: number): number { return a + b; }`; const pos = src.indexOf("add("); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4183,8 +4209,8 @@ export class Derived extends Base { `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport class Base {\n x: number = 0;\n}\nexport class Derived extends Base {\n y: string = "";\n}\n`; const pos = src.indexOf("Derived"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4211,8 +4237,8 @@ export interface Dog extends Animal { `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface Animal {\n name: string;\n}\nexport interface Dog extends Animal {\n bark(): void;\n}\n`; const pos = src.indexOf("Dog"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4237,8 +4263,8 @@ export type BoxOfString = Box; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const typeAlias = sourceFile.statements.find(isTypeAliasDeclaration); @@ -4267,8 +4293,8 @@ export const n: number = 0; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport class Base {\n x: number = 0;\n}\nexport class Derived extends Base {\n y: string = "";\n}\nexport const n: number = 0;\n`; const derivedSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("Derived")); @@ -4300,8 +4326,8 @@ declare const bad: ThisTypeDoesNotExist; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\ndeclare const good: string;\ndeclare const bad: ThisTypeDoesNotExist;\n`; const badSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("bad")); @@ -4330,8 +4356,8 @@ export type Alias = typeof value; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport const value = 1;\nexport type Alias = typeof value;\n`; // A real symbol is not the unknown/undefined symbol. @@ -4355,8 +4381,8 @@ notCallable(); "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const calls: Node[] = []; @@ -4389,8 +4415,8 @@ export const value = 1; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile); @@ -4417,8 +4443,8 @@ export const obj: { a: number } = { a: 1 }; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport const obj: { a: number } = { a: 1 };\n`; const objSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("obj")); assert.ok(objSymbol); @@ -4450,8 +4476,8 @@ export { x as '${maliciousName}' }; "/src/main.ts": source, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const typesFile = await project.program.getSourceFile("/src/types.d.ts"); assert.ok(typesFile); @@ -4524,8 +4550,8 @@ export const total = add(1, 2); `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const functions = [...sourceFile.statements].filter(isFunctionDeclaration); @@ -4586,8 +4612,8 @@ var measure = function (name) { `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.js"); assert.ok(sourceFile); const variable = sourceFile.statements.find(isVariableStatement); @@ -4621,8 +4647,8 @@ const cast = /** @type {number} */ (someValue); `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.js"); assert.ok(sourceFile); const statements = [...sourceFile.statements].filter(isVariableStatement); @@ -4652,8 +4678,8 @@ export declare const p: Person; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface Person {\n name: string;\n age: number;\n greet(): void;\n}\nexport declare const p: Person;\n`; const pos = src.indexOf("p: Person"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4681,8 +4707,8 @@ export declare const m: StringMap; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface StringMap {\n [key: string]: number;\n}\nexport declare const m: StringMap;\n`; const pos = src.indexOf("m: StringMap"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4710,8 +4736,8 @@ export declare const m: ReadonlyMap; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface ReadonlyMap {\n readonly [key: string]: number;\n}\nexport declare const m: ReadonlyMap;\n`; const pos = src.indexOf("m: ReadonlyMap"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4731,8 +4757,8 @@ describe("Checker - getConstraintOfTypeParameter", () => { "/src/main.ts": `export function identity(x: T): T { return x; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function identity(x: T): T { return x; }`; const pos = src.indexOf("identity<"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4756,8 +4782,8 @@ describe("Checker - TypeParameter getters", () => { "/src/main.ts": `export function f(x: T): T { return x; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function f(x: T): T { return x; }`; const pos = src.indexOf("f<"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4785,8 +4811,8 @@ describe("Checker - TypeParameter getters", () => { "/src/main.ts": `export function f(x: T): T { return x; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function f(x: T): T { return x; }`; const pos = src.indexOf("f<"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4811,8 +4837,8 @@ describe("Checker - getTypeArguments", () => { "/src/main.ts": `export const arr: Array = [1, 2, 3];`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const arr: Array = [1, 2, 3];`; const pos = src.indexOf("arr:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4831,8 +4857,8 @@ describe("Checker - getTypeArguments", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // `string` is not a type reference. When getTypeArguments is reached // with one, the server panics, but the per-request panic recovery @@ -4860,8 +4886,8 @@ describe("Checker - getBaseConstraintOfType", () => { "/src/main.ts": `export function identity(x: T): T { return x; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function identity(x: T): T { return x; }`; const pos = src.indexOf("identity<"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4882,8 +4908,8 @@ describe("Checker - getBaseConstraintOfType", () => { "/src/main.ts": `export const x: number = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = `export const x: number = 1;`.indexOf("x:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -4907,8 +4933,8 @@ export declare const p: Person; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface Person {\n name: string;\n age: number;\n}\nexport declare const p: Person;\n`; const pos = src.indexOf("p: Person"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4930,8 +4956,8 @@ describe("Checker - getConstantValue", () => { "/src/main.ts": `export enum E { A = 1, B = 2 }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let memberB: Node | undefined; @@ -4953,8 +4979,8 @@ describe("Checker - getConstantValue", () => { "/src/main.ts": `export enum Color { Red = "red" }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let member: Node | undefined; @@ -4975,8 +5001,8 @@ describe("Checker - getSignatureFromDeclaration", () => { "/src/main.ts": `export function add(a: number, b: number): number { return a + b; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let funcDecl: Node | undefined; @@ -5004,8 +5030,8 @@ export { value as renamed }; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let exportSpecifier: Node | undefined; @@ -5028,8 +5054,8 @@ describe("Checker - getAliasedSymbol", () => { "/src/main.ts": `import { foo } from "./foo";\nexport const usage = foo;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = `import { foo } from "./foo";`.indexOf("foo }"); const aliasSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(aliasSymbol); @@ -5053,8 +5079,8 @@ export class Standalone {} `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile); @@ -5082,8 +5108,8 @@ export * from "./inner"; `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const moduleSymbol = await project.checker.getSymbolAtLocation(sourceFile); @@ -5111,8 +5137,8 @@ function f() { test("returns symbols visible at a position", async () => { await using api = spawnAPI(scopeFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = scopeFiles["/src/main.ts"].indexOf("return innerValue"); const symbols = await project.checker.getSymbolsInScope( { document: "/src/main.ts", position: pos }, @@ -5128,8 +5154,8 @@ function f() { test("returns type symbols when asked for type meaning at a node", async () => { await using api = spawnAPI(scopeFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const symbols = await project.checker.getSymbolsInScope(sourceFile, SymbolFlags.Type); @@ -5143,8 +5169,8 @@ function f() { test("SymbolFlags.All includes both value and type meanings", async () => { await using api = spawnAPI(scopeFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = scopeFiles["/src/main.ts"].indexOf("return innerValue"); const symbols = await project.checker.getSymbolsInScope( { document: "/src/main.ts", position: pos }, @@ -5172,8 +5198,8 @@ export function add(a: number, b: number): number { return a + b; } test("getDocumentationComment returns the leading comment text", async () => { await using api = spawnAPI(docFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = docFiles["/src/main.ts"].indexOf("add(a"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5185,8 +5211,8 @@ export function add(a: number, b: number): number { return a + b; } test("getJsDocTags returns structured tag name/text pairs", async () => { await using api = spawnAPI(docFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = docFiles["/src/main.ts"].indexOf("add(a"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5208,8 +5234,8 @@ describe("TypeParameter - isThisType", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // ": this {" — offset 2 past ': ' lands on 't' in the return-type 'this' const pos = src.indexOf(": this {") + 2; const type = await project.checker.getTypeAtPosition("/src/main.ts", pos); @@ -5226,8 +5252,8 @@ describe("TypeParameter - isThisType", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // Point to 'T' in the type parameter declaration '' — getTypeAtPosition // on a type annotation reference doesn't resolve to TypeParameter, but // the declaration position does. @@ -5248,8 +5274,8 @@ describe("Type - getAliasTypeArguments", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("x:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5267,8 +5293,8 @@ describe("Type - getAliasTypeArguments", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("p:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5287,8 +5313,8 @@ describe("Type - getAliasTypeArguments", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("arr:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5308,8 +5334,8 @@ describe("Type - getAliasSymbol", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("p:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5327,8 +5353,8 @@ describe("Type - getAliasSymbol", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("c:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5346,8 +5372,8 @@ describe("Type - getAliasSymbol", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("str:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5366,8 +5392,8 @@ describe("IntrinsicType - intrinsicName", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const stringType = await project.checker.getStringType(); assert.equal((stringType as IntrinsicType).intrinsicName, "string"); const anyType = await project.checker.getAnyType(); @@ -5392,8 +5418,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("empty:")); assert.ok(symbol); const type = await project.checker.getTypeOfSymbol(symbol); @@ -5410,8 +5436,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("greeting:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5429,8 +5455,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const posSymbol = await project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("pos =")); assert.ok(posSymbol); @@ -5458,8 +5484,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("greeting:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5486,8 +5512,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("greeting:"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5513,8 +5539,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("Pending"); const symbol = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5545,8 +5571,8 @@ describe("Checker - isContextSensitive", () => { "/src/main.ts": `export const fn = (x) => x;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); // Find the arrow function node @@ -5570,8 +5596,8 @@ describe("Checker - isTypeAssignableTo", () => { "/src/main.ts": `export {};`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const stringType = await project.checker.getStringType(); const anyType = await project.checker.getAnyType(); const neverType = await project.checker.getNeverType(); @@ -5586,8 +5612,8 @@ describe("Checker - isTypeAssignableTo", () => { "/src/main.ts": `export {};`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const stringType = await project.checker.getStringType(); const numberType = await project.checker.getNumberType(); assert.ok(!await project.checker.isTypeAssignableTo(numberType, stringType), "number not assignable to string"); @@ -5601,8 +5627,8 @@ describe("Checker - isTypeAssignableTo", () => { "/src/main.ts": src, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("x:"); const sym = await project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(sym); @@ -5630,8 +5656,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created keyword type", async () => { await using api = spawnAPI(emitterFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createKeywordTypeNode(SyntaxKind.StringKeyword); const text = await project.emitter.printNode(node); assert.strictEqual(text, "string"); @@ -5640,8 +5666,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created union type", async () => { await using api = spawnAPI(emitterFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createUnionTypeNode([ createKeywordTypeNode(SyntaxKind.StringKeyword), createKeywordTypeNode(SyntaxKind.NumberKeyword), @@ -5653,8 +5679,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created function type", async () => { await using api = spawnAPI(emitterFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const param = createParameterDeclaration( undefined, undefined, @@ -5675,8 +5701,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created type reference", async () => { await using api = spawnAPI(emitterFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createTypeReferenceNode(createIdentifier("Array"), [ createKeywordTypeNode(SyntaxKind.StringKeyword), ]); @@ -5687,8 +5713,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created array type", async () => { await using api = spawnAPI(emitterFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createArrayTypeNode(createKeywordTypeNode(SyntaxKind.NumberKeyword)); const text = await project.emitter.printNode(node); assert.strictEqual(text, "number[]"); @@ -5697,8 +5723,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("typeToTypeNode + printNode round-trip", async () => { await using api = spawnAPI(emitterFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker, emitter } = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker, emitter } = snapshot.getConfiguredProject("/tsconfig.json")!; const src = emitterFiles["/src/main.ts"]; const greetPos = src.indexOf("greet("); @@ -5716,8 +5742,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("visitEachChild on typeToTypeNode result with keyword types", async () => { await using api = spawnAPI(emitterFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker } = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getConfiguredProject("/tsconfig.json")!; const src = emitterFiles["/src/main.ts"]; const objPos = src.indexOf("obj"); const symbol = await checker.getSymbolAtPosition("/src/main.ts", objPos); @@ -5764,8 +5790,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("typeToString", async () => { await using api = spawnAPI(emitterFiles); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker } = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getConfiguredProject("/tsconfig.json")!; const src = emitterFiles["/src/main.ts"]; const greetPos = src.indexOf("greet("); @@ -5783,8 +5809,8 @@ export const obj = { m: 1, s: "hi", b: true }; "/src/main.ts": `export function greet(name: string): string[] { return [name]; }`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker } = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getConfiguredProject("/tsconfig.json")!; const greetPos = "export function greet".indexOf("greet"); const symbol = await checker.getSymbolAtPosition("/src/main.ts", greetPos); assert.ok(symbol); @@ -5800,8 +5826,8 @@ export const obj = { m: 1, s: "hi", b: true }; "/src/main.ts": `const foo = /asdfasf;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -5845,8 +5871,8 @@ describe("Program - selected file emit", () => { const { api: disposableAPI, fs } = spawnAPIWithFS({ ...files }); await using api = disposableAPI; - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.getJavaScriptEmit(["/src/a.ts", "/src/b.ts"]); assert.equal(result.emitSkipped, false); assert.deepEqual([...result.outputFiles.keys()], [ @@ -5864,8 +5890,8 @@ describe("Program - selected file emit", () => { const { api: disposableAPI, fs } = spawnAPIWithFS({ ...files }); await using api = disposableAPI; - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.getDeclarationEmit(["/src/a.ts", "/src/b.ts"]); assert.equal(result.emitSkipped, false); assert.deepEqual([...result.outputFiles.keys()], [ @@ -5881,8 +5907,8 @@ describe("Program - selected file emit", () => { test("selected file emit accepts empty arrays", async () => { await using api = spawnAPI({ ...files }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; assert.deepEqual((await project.program.getJavaScriptEmit([])).outputFiles, new Map()); assert.deepEqual((await project.program.getDeclarationEmit([])).outputFiles, new Map()); }); @@ -5896,7 +5922,7 @@ describe("SnapshotInternalAPI - formatNodeForInsertion", () => { }; await using api = spawnAPI(files); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); const node = createVariableStatement( undefined, @@ -5921,7 +5947,7 @@ describe("SnapshotInternalAPI - formatNodeForInsertion", () => { }; await using api = spawnAPI(files); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); const node = createVariableStatement( undefined, @@ -5945,7 +5971,7 @@ describe("SnapshotInternalAPI - formatNodeForInsertion", () => { }; await using api = spawnAPI(files); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); const node = createVariableStatement( undefined, @@ -5973,8 +5999,8 @@ describe("modifierFlags", () => { "/src/index.ts": `export async function foo() {}`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -5997,8 +6023,8 @@ describe("modifierFlags", () => { "/src/index.ts": `function bar() {}`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -6021,8 +6047,8 @@ describe("Checker - getResolvedSymbol", () => { "/src/index.ts": `const x = 1;\nconst y = x;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -6050,8 +6076,8 @@ describe("VariableDeclarationList - BlockScoped flags", () => { "/src/index.ts": `let x = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -6072,8 +6098,8 @@ describe("VariableDeclarationList - BlockScoped flags", () => { "/src/index.ts": `const x = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -6095,8 +6121,8 @@ test("TypeOperator operator kind", async () => { "/src/index.ts": `function test(arg: readonly number[]) { }\n`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert(sourceFile); const param = (sourceFile.statements[0] as import("@typescript/typescript/unstable/ast").FunctionDeclaration).parameters[0]; @@ -6115,8 +6141,8 @@ test("SpreadAssignment roundtrip", async () => { "/src/index.ts": `var thing = { ...other };\n`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert(sourceFile); const stmt = sourceFile.statements[0] as import("@typescript/typescript/unstable/ast").VariableStatement; @@ -6137,8 +6163,8 @@ test("VariableDeclarationList const flag clone", async () => { "/src/index.ts": `const thing = 123;\n`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert(sourceFile); { @@ -6167,8 +6193,8 @@ doThing(); `, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert(sourceFile); const printed = await project.emitter.printNode(sourceFile); @@ -6178,8 +6204,8 @@ doThing(); test("Factory ModifierList auto-conversion", async () => { await using api = spawnAPI(); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createTypeAliasDeclaration( [createToken(SyntaxKind.ExportKeyword)], createIdentifier("Test"), @@ -6206,8 +6232,8 @@ test("Parse-clone-emit roundtrip", async () => { const errors = { ...target }; for (const tsconfig of globSync("**/tsconfig.json", { cwd: tsSource })) { - const snapshot = await api.updateSnapshot({ openProject: resolve(tsSource, tsconfig) }); - const project = snapshot.getProject(tsconfig); + const snapshot = await api.createSnapshot({ openProject: resolve(tsSource, tsconfig) }); + const project = snapshot.getConfiguredProject(tsconfig); assert(project); for (const file of project.rootFiles) { const source = await project.program.getSourceFile(file); @@ -6251,8 +6277,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": source, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getSyntacticDiagnostics("/src/index.ts"); assert.deepEqual(diags[0].startPosition, { line: 0, character: 9 }); assert.deepEqual(diags[0].endPosition, { line: 0, character: 10 }); @@ -6273,8 +6299,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": source, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getSemanticDiagnostics("/src/index.ts"); const declRange = rangeOf(source, "callback", 0); const assignRange = rangeOf(source, "callback", 1); @@ -6315,8 +6341,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": source, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getSuggestionDiagnostics("/src/index.ts"); assert.deepEqual(withoutFormattingContext(diags), [{ fileName: "/src/index.ts", @@ -6335,8 +6361,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getConfigFileParsingDiagnostics(); assert.deepEqual(withoutFormattingContext(diags), [{ fileName: "/tsconfig.json", @@ -6356,8 +6382,8 @@ describe("Program - diagnostics", () => { }); await using api = disposableAPI; - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const names = await project.program.getConfigFileNames(); assert.deepEqual(names, ["/tsconfig.json", "/tsconfig.base.json"]); @@ -6382,8 +6408,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x: number = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getDeclarationDiagnostics("/src/index.ts"); assert.deepEqual(diags, []); }); @@ -6395,8 +6421,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": source, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getBindDiagnostics("/src/index.ts"); assert.deepEqual(withoutFormattingContext(diags), [ { @@ -6423,8 +6449,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getProgramDiagnostics(); assert.deepEqual(withoutFormattingContext(diags), [ { @@ -6450,8 +6476,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getGlobalDiagnostics(); assert.deepEqual(diags, []); }); @@ -6462,8 +6488,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x = [1, 2, 3];`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getGlobalDiagnostics(); // With noLib, the checker reports "Cannot find global type" diagnostics that // are not associated with any source file. @@ -6489,8 +6515,8 @@ describe("Program - diagnostics", () => { "/src/clean.ts": `const c = 3;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getSyntacticDiagnostics(["/src/a.ts", "/src/b.ts"]); assert.deepEqual(withoutFormattingContext(diags), [ { @@ -6519,8 +6545,8 @@ describe("Program - diagnostics", () => { "/src/b.ts": sourceB, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getSemanticDiagnostics(["/src/a.ts", "/src/b.ts"]); assert.equal(diags.length, 2); assert.equal(diags[0].fileName, "/src/a.ts"); @@ -6538,8 +6564,8 @@ describe("Program - diagnostics", () => { "/src/b.ts": sourceB, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getBindDiagnostics(["/src/a.ts", "/src/b.ts"]); assert.equal(diags.length, 4); assert.equal(diags.filter(d => d.fileName === "/src/a.ts").length, 2); @@ -6553,8 +6579,8 @@ describe("Program - diagnostics", () => { "/src/b.ts": `const b: = 2;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getSyntacticDiagnostics(); assert.equal(diags.length, 2); }); @@ -6566,8 +6592,8 @@ describe("Program - diagnostics", () => { "/src/b.ts": `const b: = 2;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = await project.program.getSyntacticDiagnostics([]); assert.deepEqual(diags, []); }); @@ -6582,8 +6608,8 @@ describe("getDefaultProjectForFile", () => { "/node_modules/my-lib/index.d.ts": `export declare const foo: string;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // The d.ts is not imported, so it is not in the project's program const dtsSf = await project.program.getSourceFile("/node_modules/my-lib/index.d.ts"); @@ -6593,8 +6619,8 @@ describe("getDefaultProjectForFile", () => { const noProject = await snapshot.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"); assert.equal(noProject, undefined, "getDefaultProjectForFile returns undefined for unloaded file"); - // Load the file into the inferred project via updateSnapshot openFiles - const snapshot2 = await api.updateSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); + // Load the file into the inferred project via createSnapshot openFiles + const snapshot2 = await api.createSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); const defaultProject = await snapshot2.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"); assert.ok(defaultProject, "getDefaultProjectForFile should find inferred project after openFiles"); @@ -6610,7 +6636,7 @@ describe("getDefaultProjectForFile", () => { }); await using api = disposableAPI; - const snapshot1 = await api.updateSnapshot({ openFiles: ["/loose.ts"] }); + const snapshot1 = await api.createSnapshot({ openFiles: ["/loose.ts"] }); const project1 = await snapshot1.getDefaultProjectForFile("/loose.ts"); assert.ok(project1, "file with no config file in its ancestry should load into the inferred project"); const sf1 = await project1.program.getSourceFile("/loose.ts"); @@ -6619,7 +6645,8 @@ describe("getDefaultProjectForFile", () => { // Mutate the file and notify only via fileChanges — no follow-up openFiles/closeFiles. fs.writeFile!("/loose.ts", `export const foo = 2;`); - const snapshot2 = await api.updateSnapshot({ + const snapshot2 = await api.createSnapshot({ + openFiles: ["/loose.ts"], fileChanges: { changed: ["/loose.ts"] }, }); @@ -6634,7 +6661,7 @@ describe("getDefaultProjectForFile", () => { ); }); - test("keeps previously opened files open across subsequent openFiles calls", async () => { + test("opens multiple inferred files in one snapshot", async () => { await using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), "/src/index.ts": `export const x = 1;`, @@ -6644,11 +6671,9 @@ describe("getDefaultProjectForFile", () => { "/node_modules/other-lib/index.d.ts": `export declare const bar: number;`, }); - await api.updateSnapshot({ openProject: "/tsconfig.json" }); - await api.updateSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); - - // Opening a second file in a later snapshot must not close the first one. - const snapshot = await api.updateSnapshot({ openFiles: ["/node_modules/other-lib/index.d.ts"] }); + const snapshot = await api.createSnapshot({ + openFiles: ["/node_modules/my-lib/index.d.ts", "/node_modules/other-lib/index.d.ts"], + }); const firstProject = await snapshot.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"); assert.ok(firstProject, "previously opened file should remain in the inferred project"); @@ -6664,7 +6689,7 @@ describe("getDefaultProjectForFile", () => { // Open the file without first opening the project. Like LSP's didOpen, this // should search ancestor directories for a tsconfig that contains the file. - const snapshot = await api.updateSnapshot({ openFiles: ["/src/index.ts"] }); + const snapshot = await api.createSnapshot({ openFiles: ["/src/index.ts"] }); const defaultProject = await snapshot.getDefaultProjectForFile("/src/index.ts"); assert.ok(defaultProject, "should find a project for the opened file"); assert.equal( @@ -6680,12 +6705,12 @@ describe("getDefaultProjectForFile", () => { "/src/index.ts": `export const x = 1;`, }); - const opened = await api.updateSnapshot({ openProjects: ["/tsconfig.json"] }); - assert.ok(opened.getProject("/tsconfig.json"), "project should be open after openProjects"); + const opened = await api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + assert.ok(opened.getConfiguredProject("/tsconfig.json"), "project should be open after openProjects"); - const closed = await api.updateSnapshot({ closeProjects: ["/tsconfig.json"] }); + const closed = await api.createSnapshot({ closeProjects: ["/tsconfig.json"] }); assert.equal( - closed.getProject("/tsconfig.json"), + closed.getConfiguredProject("/tsconfig.json"), undefined, "project should be unloaded after closeProjects", ); @@ -6699,14 +6724,14 @@ describe("getDefaultProjectForFile", () => { "/node_modules/my-lib/index.d.ts": `export declare const foo: string;`, }); - await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const opened = await api.updateSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); + await api.createSnapshot({ openProject: "/tsconfig.json" }); + const opened = await api.createSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); assert.ok( await opened.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"), "file should resolve to a project after openFiles", ); - const closed = await api.updateSnapshot({ closeFiles: ["/node_modules/my-lib/index.d.ts"] }); + const closed = await api.createSnapshot({ closeFiles: ["/node_modules/my-lib/index.d.ts"] }); assert.equal( await closed.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"), undefined, @@ -6735,8 +6760,8 @@ describe("Program - emit", () => { fs: fs, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.emit(); assert.deepEqual(result, { diagnostics: [], @@ -6767,8 +6792,8 @@ describe("Program - emit", () => { fs: fs, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.emit(EmitOnly.OnlyDts); assert.deepEqual(result, { diagnostics: [], @@ -6797,8 +6822,8 @@ describe("Program - emit", () => { fs: fs, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.emit(EmitOnly.OnlyJs); assert.deepEqual(result, { diagnostics: [], @@ -6823,8 +6848,8 @@ describe("Program - emit", () => { const { api: disposableAPI, fs } = spawnAPIWithFS({ ...files }); await using api = disposableAPI; - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.emitToString(EmitOnly.OnlyDts); assert.deepEqual([...result.outputFiles.keys()], [ "/dist/src/index.d.ts", @@ -6847,8 +6872,8 @@ describe("Program - emit", () => { }); await using api = disposableAPI; - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.emit(); assert.deepEqual( @@ -6889,8 +6914,8 @@ describe("Program - emit", () => { }); await using api = disposableAPI; - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.emit(); assert.equal(result.emitSkipped, true); assert.ok(result.diagnostics.some(d => d.code === 1109)); @@ -6911,8 +6936,8 @@ describe("Program - emit", () => { }); await using api = disposableAPI; - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; assert.deepEqual(await project.program.emit(), { diagnostics: [], emitSkipped: false, @@ -6929,8 +6954,8 @@ describe("Program - emit", () => { test("emit rejects unknown files and invalid emitOnly values", async () => { await using api = spawnAPI({ ...files }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; let error: unknown; try { @@ -6962,8 +6987,8 @@ describe("Program - emit", () => { fs, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = await project.program.emit(); assert.deepEqual(result.emittedFiles, []); assert.ok(result.diagnostics.some(d => d.text.includes("write failed"))); @@ -6990,8 +7015,8 @@ describe("Timing", () => { assert.equal(info.recentRequests.length, 0); // Exercise a JSON request and a binary source-file request. - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -7049,8 +7074,8 @@ describe("Timing", () => { collectTiming: true, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = await project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -7118,21 +7143,21 @@ describe("runWithTemporaryFileUpdate", () => { "/src/index.ts": `export const x: number = 1;`, }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // The original content type-checks cleanly. const baseDiags = await project.program.getSemanticDiagnostics("/src/index.ts"); assert.equal(baseDiags.length, 0); // Keep a newer snapshot active to verify any active snapshot can be the base. - const latestSnapshot = await api.updateSnapshot(); + const latestSnapshot = await api.createSnapshot(); assert.notEqual(latestSnapshot.id, snapshot.id); // Inside the callback, the file has the temporary (erroneous) content. let errorCount = -1; await api.runWithTemporaryFileUpdate(snapshot, "/src/index.ts", `export const x: string = 1;`, async tempSnapshot => { - const tempProject = tempSnapshot.getProject("/tsconfig.json")!; + const tempProject = tempSnapshot.getConfiguredProject("/tsconfig.json")!; const diags = await tempProject.program.getSemanticDiagnostics("/src/index.ts"); errorCount = diags.length; }); @@ -7142,12 +7167,35 @@ describe("runWithTemporaryFileUpdate", () => { const afterDiags = await project.program.getSemanticDiagnostics("/src/index.ts"); assert.equal(afterDiags.length, 0); - // Subsequent regular updates still work and diff against the real latest snapshot. - const snapshot2 = await api.updateSnapshot(); - const project2 = snapshot2.getProject("/tsconfig.json")!; + // A subsequent independent snapshot can request the project again. + const snapshot2 = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project2 = snapshot2.getConfiguredProject("/tsconfig.json")!; const diags2 = await project2.program.getSemanticDiagnostics("/src/index.ts"); assert.equal(diags2.length, 0); }); + + test("reconstructs projects omitted from the response diff", async () => { + await using api = spawnAPI({ + "/first/tsconfig.json": `{}`, + "/first/index.ts": `export const first = 1;`, + "/second/tsconfig.json": `{}`, + "/second/index.ts": `export const second = 1;`, + }); + + const snapshot = await api.createSnapshot({ + openProjects: ["/first/tsconfig.json", "/second/tsconfig.json"], + }); + const originalSecondProject = snapshot.getConfiguredProject("/second/tsconfig.json")!; + + await api.runWithTemporaryFileUpdate(snapshot, "/first/index.ts", `export const first = 2;`, async tempSnapshot => { + assert.equal(tempSnapshot.getProjects().length, 2); + assert.ok(tempSnapshot.getConfiguredProject("/first/tsconfig.json")); + const secondProject = tempSnapshot.getConfiguredProject("/second/tsconfig.json"); + assert.ok(secondProject); + assert.notStrictEqual(secondProject, originalSecondProject); + assert.equal((await secondProject.program.getSourceFileNames()).includes("/second/index.ts"), true); + }); + }); }); function spawnAPIWithFS(files: Record = { ...defaultFiles }): { api: API; fs: FileSystem; } { diff --git a/packages/typescript/test/async/astnav.test.ts b/packages/typescript/test/async/astnav.test.ts index d4285c7fd53e1..0200611cd8727 100644 --- a/packages/typescript/test/async/astnav.test.ts +++ b/packages/typescript/test/async/astnav.test.ts @@ -103,8 +103,8 @@ describe("astnav", () => { }), }); - const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = await api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sf = await project.program.getSourceFile("/src/testFile.ts"); assert.ok(sf, "Failed to get source file from API"); sourceFile = sf; diff --git a/packages/typescript/test/diagnosticFormatter.test.ts b/packages/typescript/test/diagnosticFormatter.test.ts index bce9f0f98371a..4cefa0b845f8b 100644 --- a/packages/typescript/test/diagnosticFormatter.test.ts +++ b/packages/typescript/test/diagnosticFormatter.test.ts @@ -18,8 +18,8 @@ describe("diagnosticFormatter", () => { "/project/index.ts": source, }); try { - const snapshot = await api.updateSnapshot({ openProject: "/project/tsconfig.json" }); - const program = snapshot.getProject("/project/tsconfig.json")!.program; + const snapshot = await api.createSnapshot({ openProject: "/project/tsconfig.json" }); + const program = snapshot.getConfiguredProject("/project/tsconfig.json")!.program; const diagnostics = await program.getSemanticDiagnostics("/project/index.ts"); assert.equal(diagnostics.length, 1); assert.equal(api.getCurrentDirectory(), "/workspace"); @@ -87,8 +87,8 @@ describe("diagnosticFormatter", () => { "/workspace/index.ts": `const x: number = "oops";`, }); try { - const snapshot = await api.updateSnapshot({ openProject: "/workspace/tsconfig.json" }); - const program = snapshot.getProject("/workspace/tsconfig.json")!.program; + const snapshot = await api.createSnapshot({ openProject: "/workspace/tsconfig.json" }); + const program = snapshot.getConfiguredProject("/workspace/tsconfig.json")!.program; const diagnostics = await program.getSemanticDiagnostics("/workspace/index.ts"); const configDiagnostics = (await api.parseConfigFile("/workspace/tsconfig.json")).errors; const clonedDiagnostics = [ @@ -113,7 +113,7 @@ describe("diagnosticFormatter", () => { "/workspace/index.ts": `const x: number = "oops";`, }); try { - const snapshot = await api.updateSnapshot({ openFiles: ["/workspace/index.ts"] }); + const snapshot = await api.createSnapshot({ openFiles: ["/workspace/index.ts"] }); const project = await snapshot.getDefaultProjectForFile("/workspace/index.ts"); assert.ok(project); assert.equal(project.program.getCurrentDirectory(), api.getCurrentDirectory()); diff --git a/packages/typescript/test/generators/api.bench.ts b/packages/typescript/test/generators/api.bench.ts index a190c244b2557..ddc3328e827b0 100644 --- a/packages/typescript/test/generators/api.bench.ts +++ b/packages/typescript/test/generators/api.bench.ts @@ -200,7 +200,7 @@ export function runBenchmarks(options?: { filter?: string; singleIteration?: boo } function loadSnapshot() { - [snapshot] = api.batch(api.updateSnapshot.gen({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" })); + [snapshot] = api.batch(api.createSnapshot.gen({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" })); project = snapshot.getProjects()[0]; } diff --git a/packages/typescript/test/sync/api-generators.test.ts b/packages/typescript/test/sync/api-generators.test.ts index 73adc15b31486..bca694c42643f 100644 --- a/packages/typescript/test/sync/api-generators.test.ts +++ b/packages/typescript/test/sync/api-generators.test.ts @@ -143,6 +143,7 @@ interface ParityCase { const exercisedMethods = new Set(); const publicGeneratorExemptions = new Map([ ["API.fromLSPConnection", "requires an existing LSP API session"], + ["API.getCurrentLanguageServerSnapshot", "requires an existing LSP API session"], ["InternalAPI.startCPUProfile", "writes a CPU profile and changes process-global profiling state"], ["InternalAPI.stopCPUProfile", "requires a matching active CPU profile"], ["InternalAPI.saveHeapProfile", "writes a potentially large heap profile to disk"], @@ -150,8 +151,7 @@ const publicGeneratorExemptions = new Map([ const privateGeneratorGetters = new Set([ "API.ensureInitialized", "API.initializeWorker", - "API.updateSnapshotFrom", - "API.updateSnapshotWorker", + "API.updateSnapshot", "Checker.getIntrinsicType", "Checker.getWellKnownSignatures", "Checker.getWellKnownSymbols", @@ -311,7 +311,9 @@ function assertOptionalSourceFilesEquivalent(actual: SourceFile | undefined, exp } function assertProjectsEquivalent(actual: Project, expected: Project, message?: string): void { + assert.equal(actual.id, expected.id, message); assert.equal(actual.configFileName, expected.configFileName, message); + assert.equal(actual.dirty, expected.dirty, message); assert.deepEqual(actual.rootFiles, expected.rootFiles, message); } @@ -327,6 +329,8 @@ function assertSnapshotsEquivalent(actual: Snapshot, expected: Snapshot, message const actualProjects = actual.getProjects(); const expectedProjects = expected.getProjects(); assertArrayElementsEquivalent(actualProjects, expectedProjects, assertProjectsEquivalent, message); + assert.deepEqual(actual.operation.createdPrograms?.map(program => program.id), expected.operation.createdPrograms?.map(program => program.id), message); + assert.deepEqual(actual.operation.openedFiles?.map(result => result.project.id), expected.operation.openedFiles?.map(result => result.project.id), message); } function assertSymbolMapsEquivalent(actual: ReadonlyMap, expected: ReadonlyMap, message?: string): void { @@ -1288,8 +1292,8 @@ describe("API - generator batching", () => { test("yields source file metadata requests on cache misses", () => { const api = spawnAPI(); try { - using snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const program = snapshot.getProject("/tsconfig.json")!.program; + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const sourceFile = program.getSourceFile("/src/index.ts")!; const state = program.getSourceFileMetadataByPath.gen(sourceFile.path).next(); @@ -1304,8 +1308,8 @@ describe("API - generator batching", () => { test("uses generators attached to sync API methods", () => { const api = spawnAPI(); try { - using snapshot = api.batch(api.updateSnapshot.gen({ openProject: "/tsconfig.json" }))[0]; - const project = snapshot.getProject("/tsconfig.json")!; + using snapshot = api.batch(api.createSnapshot.gen({ openProject: "/tsconfig.json" }))[0]; + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const node = cast( @@ -1356,8 +1360,8 @@ describe("API - generator batching", () => { test("keeps every publicly reachable generator-backed method in sync", () => { const api = spawnAPI(parityFiles); try { - using snapshot = api.batch(api.updateSnapshot.gen({ openProject: "/tsconfig.json" }))[0]; - const project = snapshot.getProject("/tsconfig.json")!; + using snapshot = api.batch(api.createSnapshot.gen({ openProject: "/tsconfig.json" }))[0]; + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const { checker, emitter, languageService, program } = project; const indexFile = program.getSourceFile("/src/index.ts")!; const modelsFile = program.getSourceFile("/src/models.ts")!; @@ -1499,12 +1503,13 @@ describe("API - generator batching", () => { parityCase("API", "transpileModuleFromFile", api.transpileModuleFromFile, assertDeepEquivalent, "/src/index.ts"), parityCase("API", "transpileDeclaration", api.transpileDeclaration, assertDeepEquivalent, "export function declared(value: string): number { return value.length; }"), parityCase("API", "transpileDeclarationFromFile", api.transpileDeclarationFromFile, assertDeepEquivalent, "/src/index.ts"), - parityCase("API", "updateSnapshot", api.updateSnapshot, assertSnapshotsEquivalent, { openProject: "/tsconfig.json" }), + parityCase("API", "createSnapshot", api.createSnapshot as GeneratorMethod<[params: { openProject: string; }], Snapshot>, assertSnapshotsEquivalent, { openProject: "/tsconfig.json" }), parityCase("API", "createProgram", api.createProgram, assertProgramsEquivalent, ["/src/index.ts"], { compilerOptions: { noLib: true } }), parityCase("API", "runWithTemporaryFileUpdate", api.runWithTemporaryFileUpdate, assertDeepEquivalent, snapshot, "/src/index.ts", parityFiles["/src/index.ts"].replace("123", '"fixed"'), (temporarySnapshot: Snapshot) => { temporaryProjects.push(temporarySnapshot.getProjects()[0].configFileName); }), parityCase("Snapshot", "getDefaultProjectForFile", snapshot.getDefaultProjectForFile, assertOptionalProjectsEquivalent, "/src/index.ts"), + parityCase("Snapshot", "update", snapshot.update, assertSnapshotsEquivalent), parityCase("Project", "getImportAdderEdits", project.getImportAdderEdits, assertDeepEquivalent, "/src/index.ts", [{ kind: "importSymbol", symbol: unimportedSymbol }]), parityCase("Project", "getImportEditsForSymbols", project.getImportEditsForSymbols, assertDeepEquivalent, "/src/index.ts", [unimportedSymbol]), @@ -1682,8 +1687,8 @@ describe("API - generator batching", () => { const snapshotGeneratorAPI = spawnAPI(parityFiles); const snapshotSyncAPI = spawnAPI(parityFiles); try { - const generatorBase = snapshotGeneratorAPI.batch(snapshotGeneratorAPI.updateSnapshot.gen({ openProject: "/tsconfig.json" }))[0]; - const syncBase = snapshotSyncAPI.updateSnapshot({ openProject: "/tsconfig.json" }); + const generatorBase = snapshotGeneratorAPI.batch(snapshotGeneratorAPI.createSnapshot.gen({ openProject: "/tsconfig.json" }))[0]; + const syncBase = snapshotSyncAPI.createSnapshot({ openProject: "/tsconfig.json" }); const generatorUpdated = snapshotGeneratorAPI.batch(generatorBase.update.gen())[0]; const syncUpdated = syncBase.update(); assertSnapshotsEquivalent(generatorUpdated, syncUpdated, "Snapshot.update"); @@ -1695,7 +1700,7 @@ describe("API - generator batching", () => { } const destructiveAPI = spawnAPI(parityFiles); - const disposableSnapshot = destructiveAPI.batch(destructiveAPI.updateSnapshot.gen({ openProject: "/tsconfig.json" }))[0]; + const disposableSnapshot = destructiveAPI.batch(destructiveAPI.createSnapshot.gen({ openProject: "/tsconfig.json" }))[0]; destructiveAPI.batch(disposableSnapshot.dispose.gen()); assert.equal(disposableSnapshot.isDisposed(), true); assert.equal(disposableSnapshot.dispose(), undefined); diff --git a/packages/typescript/test/sync/api.bench.ts b/packages/typescript/test/sync/api.bench.ts index 5631efc706b61..08f1fb4ea8d8b 100644 --- a/packages/typescript/test/sync/api.bench.ts +++ b/packages/typescript/test/sync/api.bench.ts @@ -204,7 +204,7 @@ export function runBenchmarks(options?: { filter?: string; singleIteration?: boo } function loadSnapshot() { - snapshot = api.updateSnapshot({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" }); + snapshot = api.createSnapshot({ openProject: "tsc/testdata/fixtures/compiler/tsconfig.json" }); project = snapshot.getProjects()[0]; } diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index b8c648a5ec343..eb32e63f5404d 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -36,6 +36,7 @@ import { type Node, type NodeArray, NodeFlags, + type Path, SyntaxKind, tryGetAmbientModuleNameFromSymbolName, unescapeLeadingUnderscores, @@ -68,6 +69,7 @@ import { type BigIntLiteralType, CheckFlags, type ConditionalType, + type ConfiguredProjectId, DiagnosticCategory, type DocumentIdentifier, EmitOnly, @@ -75,6 +77,7 @@ import { type ImportAdderAction, type IndexedAccessType, type IndexType, + type InferredProjectId, type InterfaceType, type IntrinsicType, isErrorType, @@ -82,11 +85,15 @@ import { ModifierFlags, ModuleKind, ObjectFlags, + type Program, + type Project, + type ProjectId, type Signature, SignatureKind, type Snapshot, type StringMappingType, SymbolFlags, + type SyntheticProjectId, type TemplateLiteralType, type TextEdit, TypeFlags, @@ -112,6 +119,30 @@ import { } from "./api.testUtils.ts"; describe("API", () => { + test("getCurrentLanguageServerSnapshot is LSP-only", () => { + if (!!false) { + const standalone = new API(); + // @ts-expect-error The standalone API has no canonical language server state. + void standalone.getCurrentLanguageServerSnapshot(); + + const lsp = undefined! as API; + void lsp.getCurrentLanguageServerSnapshot({ openProjects: ["/tsconfig.json"] }); + const baseSnapshot = undefined! as Snapshot; + void lsp.getCurrentLanguageServerSnapshot(undefined, baseSnapshot); + + const configured = undefined! as ConfiguredProjectId; + const inferred = undefined! as InferredProjectId; + const synthetic = undefined! as SyntheticProjectId; + const path: Path = configured; + const projectIds: ProjectId[] = [configured, inferred, synthetic]; + void path; + void projectIds; + // @ts-expect-error Project ID brands are not interchangeable. + const invalid: ConfiguredProjectId = synthetic; + void invalid; + } + }); + test("initializes once for concurrent first requests", () => { using api = spawnAPI(); const [commandLine, config] = api.batch( @@ -316,6 +347,34 @@ describe("API", () => { assert.throws(() => program.getSourceFileNames(), /snapshot .* not found/); }); + test("createSnapshot creates independent synthetic programs", () => { + using api = spawnAPI({ + "/src/a.ts": `export const a = 1;`, + "/src/b.ts": `export const b = 2;`, + }); + + const snapshot = api.createSnapshot({ + createPrograms: [ + { rootFiles: ["/src/a.ts"], options: { compilerOptions: { noLib: true } } }, + { rootFiles: ["/src/b.ts"], options: { compilerOptions: { noLib: true, strict: true } } }, + ], + }); + assert.equal(snapshot.getProjects().length, 2); + assert.deepEqual(snapshot.getProjects().map(project => project.rootFiles), [["/src/a.ts"], ["/src/b.ts"]]); + assert.equal(snapshot.operation.createdPrograms!.length, 2); + for (const program of snapshot.operation.createdPrograms!) { + const syntheticProjectId: SyntheticProjectId = program.id; + void syntheticProjectId; + assert.strictEqual(snapshot.getProgram(program.id), program); + assert.strictEqual(snapshot.getProject(program.id), program.getProject()); + } + + const empty = api.createSnapshot(); + assert.deepEqual(empty.getProjects(), []); + assert.equal("createdPrograms" in empty.operation, false); + assert.equal("openedFiles" in empty.operation, false); + }); + test("createProgram ignores an on-disk tsconfig", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ @@ -376,41 +435,6 @@ describe("API", () => { program.dispose(); }); - test("createProgram updates roots when given an old program", () => { - const options = { compilerOptions: { noLib: true } }; - using api = spawnAPI({ - "/src/a.ts": `export const a = 1;`, - "/src/b.ts": `export const b = 1;`, - "/src/c.ts": `export const c = 1;`, - }); - - const oldProgram = api.createProgram(["/src/a.ts", "/src/b.ts"], options); - const newProgram = api.createProgram(["/src/a.ts", "/src/c.ts"], options, oldProgram); - assert.deepEqual(newProgram.getSourceFileNames(), ["/src/a.ts", "/src/c.ts"]); - assert.deepEqual(oldProgram.getSourceFileNames(), ["/src/a.ts", "/src/b.ts"]); - - newProgram.dispose(); - oldProgram.dispose(); - }); - - test("createProgram rejects an inactive or foreign old program", () => { - const options = { compilerOptions: { noLib: true } }; - using api = spawnAPI({ "/src/index.ts": `export const local = 1;` }); - using otherAPI = spawnAPI({ "/src/index.ts": `export const foreign = 1;` }); - - const localProgram = api.createProgram(["/src/index.ts"], options); - const foreignProgram = otherAPI.createProgram(["/src/index.ts"], options); - - const createFromForeignProgram = () => api.createProgram(["/src/index.ts"], options, foreignProgram); - assert.throws(createFromForeignProgram, /oldProgram must belong to this API instance and reference an active snapshot/); - - localProgram.dispose(); - const createFromDisposedProgram = () => api.createProgram(["/src/index.ts"], options, localProgram); - assert.throws(createFromDisposedProgram, /oldProgram must belong to this API instance and reference an active snapshot/); - - foreignProgram.dispose(); - }); - test("createProgram discovers imported non-root dependencies", () => { using api = spawnAPI({ "/src/main.ts": `import { dependency } from "./dependency"; export const value = dependency;`, @@ -423,95 +447,6 @@ describe("API", () => { program.dispose(); }); - test("createProgram updates an old program with file changes", () => { - const fileName = "/src/index.ts"; - const options = { compilerOptions: { noLib: true, strict: true } }; - const { api: disposableAPI, fs } = spawnAPIWithFS({ - [fileName]: `export const value: string = 1;`, - }); - using api = disposableAPI; - - const oldProgram = api.createProgram([fileName], options); - assert.equal((oldProgram.getSemanticDiagnostics(fileName)).length, 1); - - fs.writeFile!(fileName, `export const value: string = "valid";`); - const newProgram = api.createProgram( - [fileName], - options, - oldProgram, - { changed: [fileName] }, - ); - - assert.equal((newProgram.getSemanticDiagnostics(fileName)).length, 0); - assert.equal((oldProgram.getSemanticDiagnostics(fileName)).length, 1); - - newProgram.dispose(); - oldProgram.dispose(); - }); - - test("createProgram updates an old program with invalidateAll", () => { - const fileName = "/src/index.ts"; - const options = { compilerOptions: { noLib: true, strict: true } }; - const { api: disposableAPI, fs } = spawnAPIWithFS({ - [fileName]: `export const value: string = 1;`, - }); - using api = disposableAPI; - - const oldProgram = api.createProgram([fileName], options); - assert.equal((oldProgram.getSemanticDiagnostics(fileName)).length, 1); - - fs.writeFile!(fileName, `export const value: string = "valid";`); - const newProgram = api.createProgram( - [fileName], - options, - oldProgram, - { invalidateAll: true }, - ); - - assert.equal((newProgram.getSemanticDiagnostics(fileName)).length, 0); - assert.equal((oldProgram.getSemanticDiagnostics(fileName)).length, 1); - - newProgram.dispose(); - oldProgram.dispose(); - }); - - test("createProgram accepts a regular project program as the old program", () => { - const fileName = "/src/index.ts"; - const { api: disposableAPI, fs } = spawnAPIWithFS({ - "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, strict: true } }), - [fileName]: `export const value: string = 1;`, - }); - using api = disposableAPI; - - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; - assert.equal((project.program.getSemanticDiagnostics(fileName)).length, 1); - - fs.writeFile!(fileName, `export const value: string = "valid";`); - const newProgram = api.createProgram( - project.parsedCommandLine.fileNames, - { - compilerOptions: project.parsedCommandLine.options, - ...(project.parsedCommandLine.projectReferences - ? { projectReferences: project.parsedCommandLine.projectReferences } - : {}), - }, - project.program, - { changed: [fileName] }, - ); - - assert.equal((newProgram.getSemanticDiagnostics(fileName)).length, 0); - assert.equal((project.program.getSemanticDiagnostics(fileName)).length, 1); - newProgram.dispose(); - }); - - test("createProgram rejects file changes without an old program", () => { - using api = spawnAPI({ "/src/index.ts": `export const value = 1;` }); - - const createWithChanges = () => api.createProgram(["/src/index.ts"], { compilerOptions: { noLib: true } }, undefined, { changed: ["/src/index.ts"] }); - assert.throws(createWithChanges, /fileChanges requires an oldProgram/); - }); - test("parseConfigFile", () => { using api = spawnAPI(); @@ -583,8 +518,8 @@ describe("Checker - getImmediateAliasedSymbol", () => { "/src/main.ts": `import { foo } from "./foo";\nexport const usage = foo;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = `import { foo } from "./foo";`.indexOf("foo }"); const aliasSymbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(aliasSymbol); @@ -611,8 +546,8 @@ test(); `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const nodes: Array = []; @@ -633,14 +568,14 @@ test(); }); describe("Snapshot", () => { - test("updateSnapshot returns snapshot with projects", () => { + test("createSnapshot returns snapshot with projects", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); assert.ok(snapshot); assert.ok(snapshot.id); assert.ok(snapshot.getProjects().length > 0); - assert.ok(snapshot.getProject("/tsconfig.json")); + assert.ok(snapshot.getConfiguredProject("/tsconfig.json")); }); test("project exposes parsedCommandLine", () => { @@ -649,8 +584,8 @@ describe("Snapshot", () => { "/tsconfig.json": JSON.stringify({ compileOnSave: true }), }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; assert.deepEqual(project.parsedCommandLine.fileNames, ["/src/index.ts", "/src/foo.ts"]); assert.deepEqual(project.parsedCommandLine.options, { configFilePath: "/tsconfig.json" }); assert.equal(project.parsedCommandLine.compileOnSave, true); @@ -661,8 +596,8 @@ describe("Snapshot", () => { test("getSymbolAtPosition", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); assert.equal(symbol.name, "foo"); @@ -672,8 +607,8 @@ describe("Snapshot", () => { test("getSymbolAtLocation", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const node = cast( @@ -690,8 +625,8 @@ describe("Snapshot", () => { test("getSymbolOfSourceFile", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const moduleSymbol = project.checker.getSymbolOfSourceFile("/src/foo.ts"); assert.ok(moduleSymbol); const exports = moduleSymbol.getExports(); @@ -704,8 +639,8 @@ describe("Snapshot", () => { "/src/script.ts": `const x = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolOfSourceFile("/src/script.ts"); assert.equal(symbol, undefined); }); @@ -713,8 +648,8 @@ describe("Snapshot", () => { test("getSymbolOfSourceFile batched", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbols = project.checker.getSymbolOfSourceFile(["/src/index.ts", "/src/foo.ts"]); assert.equal(symbols.length, 2); assert.ok(symbols[0]); @@ -725,8 +660,8 @@ describe("Snapshot", () => { test("getTypeOfSymbol", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); const type = project.checker.getTypeOfSymbol(symbol); @@ -752,8 +687,8 @@ describe("Snapshot", () => { }); // when `"exactOptionalPropertyTypes": true` - const snapshot1 = api.updateSnapshot({ openProject: "/tsconfig-one.json" }); - const project1 = snapshot1.getProject("/tsconfig-one.json")!; + const snapshot1 = api.createSnapshot({ openProject: "/tsconfig-one.json" }); + const project1 = snapshot1.getConfiguredProject("/tsconfig-one.json")!; const type1 = project1.checker.getTypeAtPosition("/src/index.ts", 7); assert.ok(type1); const symbol1 = project1.checker.getPropertyOfType(type1, "a"); @@ -769,8 +704,8 @@ describe("Snapshot", () => { assert.ok(propertyType2.flags & TypeFlags.String); // when `"exactOptionalPropertyTypes": false` - const snapshot2 = api.updateSnapshot({ openProject: "/tsconfig-two.json" }); - const project2 = snapshot2.getProject("/tsconfig-two.json")!; + const snapshot2 = api.createSnapshot({ openProject: "/tsconfig-two.json" }); + const project2 = snapshot2.getConfiguredProject("/tsconfig-two.json")!; const type2 = project2.checker.getTypeAtPosition("/src/index.ts", 7); assert.ok(type2); const symbol2 = project2.checker.getPropertyOfType(type2, "a"); @@ -795,8 +730,8 @@ describe("LanguageService - imports", () => { "/src/foo.ts": `export const foo = 1;\n`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/foo.ts", "export const ".length); assert.ok(symbol); @@ -813,8 +748,8 @@ describe("LanguageService - imports", () => { "/src/foo.ts": `export const foo = 1;\nexport const bar = 2;\n`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const foo = project.checker.getSymbolAtPosition("/src/foo.ts", "export const ".length); const bar = project.checker.getSymbolAtPosition("/src/foo.ts", "export const foo = 1;\nexport const ".length); assert.ok(foo); @@ -836,8 +771,8 @@ describe("LanguageService - imports", () => { "/src/foo.ts": `export const foo = 1;\nexport const bar = 2;\n`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const bar = project.checker.getSymbolAtPosition("/src/foo.ts", "export const foo = 1;\nexport const ".length); assert.ok(bar); @@ -856,8 +791,8 @@ describe("LanguageService - imports", () => { "/src/foo.ts": `const local = 1;\n`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/foo.ts", "const ".length); assert.ok(symbol); @@ -871,8 +806,8 @@ describe("LanguageService - imports", () => { test("getImportAdderEdits rejects invalid actions", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/foo.ts", 13); assert.ok(symbol); @@ -895,8 +830,8 @@ describe("LanguageService - getCompletionsAtPosition", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // Position right after "obj." — member completion trigger const pos = src.indexOf("obj.") + "obj.".length; const completions = project.languageService.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: "." }); @@ -914,8 +849,8 @@ describe("LanguageService - getCompletionsAtPosition", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("obj.") + "obj.".length; const completions = project.languageService.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: "." }); assert.ok(completions); @@ -929,8 +864,8 @@ describe("LanguageService - getCompletionsAtPosition", () => { "/src/main.ts": `export {};`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const completions = project.languageService.getCompletionsAtPosition("/src/does-not-exist.ts", 0); assert.equal(completions, undefined, "Expected undefined for non-existent file"); }); @@ -942,8 +877,8 @@ describe("LanguageService - getCompletionsAtPosition", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("obj.") + "obj.".length; const completions = project.languageService.getCompletionsAtPosition("/src/main.ts", pos, { triggerCharacter: ".", includeSymbol: true }); assert.ok(completions, "Expected completions"); @@ -961,8 +896,8 @@ describe("LanguageService - getReferencedSymbolsForNode", () => { "/src/index.ts": `function greet(name: string) { return name; }\ngreet("world");`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const funcDecl = cast(sourceFile.statements[0], isFunctionDeclaration); @@ -983,8 +918,8 @@ describe("LanguageService - getSignatureUsage", () => { "/src/index.ts": `function greet(name: string) { return name; }\ngreet("world");`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const funcDecl = cast(sourceFile.statements[0], isFunctionDeclaration); @@ -1003,8 +938,8 @@ describe("Checker - getApparentType", () => { "/src/main.ts": `export const x = "hello" as const;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = `export const x = "hello" as const;`.indexOf("x ="); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -1041,8 +976,8 @@ class QuotaExceededError extends TaggedError("QuotaExceededError")<{ export type Result = RateLimitError | (RateLimitError & QuotaExceededError);`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const typeAlias = sourceFile.statements.find(isTypeAliasDeclaration); @@ -1063,8 +998,8 @@ describe("Checker - getMemberInModuleExports", () => { "/src/index.ts": `export const direct = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile); @@ -1081,8 +1016,8 @@ describe("SourceFile", () => { test("getSourceFile rejects invalid document identifiers", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const document = { fileName: "/src/index.ts" } as unknown as DocumentIdentifier; assert.throws( @@ -1108,8 +1043,8 @@ describe("SourceFile", () => { "/node_modules/my-lib/index.d.ts": `export declare const bar: number;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const fileNames = project.program.getSourceFileNames(); assert.deepEqual(fileNames, [ "/src/foo.ts", @@ -1130,8 +1065,8 @@ describe("SourceFile", () => { "/node_modules/my-lib/index.d.ts": `export declare const bar: number;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const program = project.program; const index = program.getSourceFile("/src/index.ts"); @@ -1167,8 +1102,8 @@ describe("SourceFile", () => { "/esm/index.ts": `export const m = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const mts = program.getSourceFile("/src/esm.mts"); assert.ok(mts); @@ -1193,8 +1128,8 @@ describe("SourceFile", () => { test("file properties", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -1205,8 +1140,8 @@ describe("SourceFile", () => { test("extended data", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -1239,8 +1174,8 @@ describe("SourceFile", () => { "/input.ts": `let arrow = () => {}`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/input.ts"); assert.ok(sourceFile); @@ -1274,8 +1209,8 @@ describe("NodeArray", () => { "/src/main.ts": `declare function foo(...args: any): void;\nfoo("a", "b",);\nfoo("a", "b");`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const statements = sourceFile.statements.filter(isExpressionStatement); @@ -1294,8 +1229,8 @@ test("unicode escapes", () => { "/src/3.ts": `"\\ud800a\\udc00"`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const expectedTexts = new Map([ ["/src/1.ts", "😃"], ["/src/2.ts", "😃"], @@ -1321,8 +1256,8 @@ test("template unicode escapes", () => { "/src/index.ts": "`\\ud800${0}\\udc00`", }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -1346,8 +1281,8 @@ test("template unicode escapes", () => { test("Object equality", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // Same symbol returned from same snapshot's checker assert.strictEqual( project.checker.getSymbolAtPosition("/src/index.ts", 9), @@ -1358,8 +1293,8 @@ test("Object equality", () => { test("Snapshot dispose", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); @@ -1370,7 +1305,7 @@ test("Snapshot dispose", () => { // After dispose, snapshot methods should throw assert.throws(() => { - snapshot.getProject("/tsconfig.json"); + snapshot.getConfiguredProject("/tsconfig.json"); }, { name: "Error", message: "Snapshot is disposed", @@ -1381,12 +1316,12 @@ describe("Multiple snapshots", () => { test("two snapshots work independently", () => { using api = spawnAPI(); - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const snap2 = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const snap2 = api.createSnapshot({ openProject: "/tsconfig.json" }); // Both can fetch source files - const sf1 = snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); - const sf2 = snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf1 = snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf2 = snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf1); assert.ok(sf2); @@ -1396,7 +1331,7 @@ describe("Multiple snapshots", () => { assert.ok(!snap2.isDisposed()); // snap2 still works after snap1 is disposed - const symbol = snap2.getProject("/tsconfig.json")!.checker.getSymbolAtPosition("/src/index.ts", 9); + const symbol = snap2.getConfiguredProject("/tsconfig.json")!.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); assert.equal(symbol.name, "foo"); }); @@ -1405,21 +1340,22 @@ describe("Multiple snapshots", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); using api = disposableAPI; - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); // Verify initial state - const sf1 = snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf1 = snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf1); assert.equal(sf1.text, `export const foo = 42;`); // Mutate the file and create a new snapshot with the change fs.writeFile!("/src/foo.ts", `export const foo = "changed";`); - const snap2 = api.updateSnapshot({ + const snap2 = api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/foo.ts"] }, }); // snap2 should reflect the updated content - const sf2 = snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf2 = snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf2); assert.equal(sf2.text, `export const foo = "changed";`); @@ -1429,14 +1365,14 @@ describe("Multiple snapshots", () => { snap1.dispose(); // snap2 still works independently after snap1 is disposed - const symbol = snap2.getProject("/tsconfig.json")!.checker.getSymbolAtPosition("/src/index.ts", 9); + const symbol = snap2.getConfiguredProject("/tsconfig.json")!.checker.getSymbolAtPosition("/src/index.ts", 9); assert.ok(symbol); snap2.dispose(); // Both are disposed, new snapshot works fine with latest content - const snap3 = api.updateSnapshot(); - const sf3 = snap3.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const snap3 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const sf3 = snap3.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf3); assert.equal(sf3.text, `export const foo = "changed";`); }); @@ -1445,20 +1381,21 @@ describe("Multiple snapshots", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); using api = disposableAPI; - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); // Add a brand new file fs.writeFile!("/src/bar.ts", `export const bar = true;`); - const snap2 = api.updateSnapshot({ + const snap2 = api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { created: ["/src/bar.ts"] }, }); - const sf = snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/bar.ts"); + const sf = snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/bar.ts"); assert.ok(sf); assert.equal(sf.text, `export const bar = true;`); // Original snapshot shouldn't have the new file - const sfOld = snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/bar.ts"); + const sfOld = snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/bar.ts"); assert.equal(sfOld, undefined); }); @@ -1466,7 +1403,7 @@ describe("Multiple snapshots", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); using api = disposableAPI; - api.updateSnapshot({ openProject: "/tsconfig.json" }); + api.createSnapshot({ openProject: "/tsconfig.json" }); const versions = [ `export const foo = 1;`, @@ -1476,22 +1413,104 @@ describe("Multiple snapshots", () => { for (const version of versions) { fs.writeFile!("/src/foo.ts", version); - const snap = api.updateSnapshot({ + const snap = api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/foo.ts"] }, }); - const sf = snap.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf = snap.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf); assert.equal(sf.text, version); } }); + + test("snapshot.update derives from its receiver and reconstructs unchanged projects", () => { + const { api: disposableAPI, fs } = spawnAPIWithFS({ + "/first/tsconfig.json": `{}`, + "/first/index.ts": `export const first = 1;`, + "/second/tsconfig.json": `{}`, + "/second/index.ts": `export const second = 1;`, + }); + using api = disposableAPI; + + const base = api.createSnapshot({ + openProjects: ["/first/tsconfig.json", "/second/tsconfig.json"], + }); + const baseFirst = base.getConfiguredProject("/first/tsconfig.json")!.program.getSourceFile("/first/index.ts"); + const baseSecondProject = base.getConfiguredProject("/second/tsconfig.json")!; + + fs.writeFile!("/first/index.ts", `export const first = 2;`); + const updated = base.update({ + fileChanges: { changed: ["/first/index.ts"] }, + ensurePrograms: [base.getConfiguredProject("/first/tsconfig.json")!.id], + }); + + assert.equal(updated.getProjects().length, 2); + assert.notStrictEqual(updated.getConfiguredProject("/second/tsconfig.json"), baseSecondProject); + assert.equal((updated.getConfiguredProject("/first/tsconfig.json")!.program.getSourceFile("/first/index.ts"))!.text, `export const first = 2;`); + assert.equal(baseFirst!.text, `export const first = 1;`); + }); + + test("snapshot.update ensures all dirty programs", () => { + const { api: disposableAPI, fs } = spawnAPIWithFS({ + "/configured/tsconfig.json": `{}`, + "/configured/index.ts": `export const configured = 1;`, + "/inferred.ts": `export const inferred = 1;`, + "/synthetic.ts": `export const synthetic = 1;`, + }); + using api = disposableAPI; + + const created = api.createSnapshot({ + openProjects: ["/configured/tsconfig.json"], + openFiles: ["/inferred.ts", "/configured/index.ts"], + createPrograms: [{ + rootFiles: ["/synthetic.ts"], + options: { compilerOptions: { noLib: true } }, + }], + }); + assert.equal(created.getProjects().length, 3); + const configuredProjectId: ConfiguredProjectId = created.getConfiguredProject("/configured/tsconfig.json")!.id; + void configuredProjectId; + assert.strictEqual(created.operation.openedFiles![0].project, created.getProject(created.operation.openedFiles![0].project.id)); + assert.strictEqual(created.operation.openedFiles![1].project, created.getConfiguredProject("/configured/tsconfig.json")); + for (const project of created.getProjects()) { + assert.equal(project.dirty, false, `${project.id} should be ensured when opened or created`); + } + + fs.writeFile!("/configured/index.ts", `export const configured = 2;`); + fs.writeFile!("/inferred.ts", `export const inferred = 2;`); + fs.writeFile!("/synthetic.ts", `export const synthetic = 2;`); + const dirty = created.update({ + fileChanges: { changed: ["/configured/index.ts", "/inferred.ts", "/synthetic.ts"] }, + }); + assert.deepEqual(dirty.getProjects().map(project => project.dirty), [true, true, true]); + + const ensured = dirty.update({ ensurePrograms: true }); + assert.deepEqual(ensured.getProjects().map(project => project.dirty), [false, false, false]); + + const withOperationResults = ensured.update({ + openFiles: ["/inferred.ts"], + createPrograms: [{ + rootFiles: ["/synthetic.ts"], + options: { compilerOptions: { noLib: true } }, + }], + }); + const openedProject: Project = withOperationResults.operation.openedFiles[0].project; + const createdProgram: Program = withOperationResults.operation.createdPrograms[0]; + const openedFilesTuple: readonly [{ readonly project: Project; }] = withOperationResults.operation.openedFiles; + const createdProgramsTuple: readonly [Program] = withOperationResults.operation.createdPrograms; + void openedFilesTuple; + void createdProgramsTuple; + assert.strictEqual(withOperationResults.getProject(openedProject.id), openedProject); + assert.strictEqual(withOperationResults.getProgram(createdProgram.id), createdProgram); + }); }); describe("Source file caching", () => { test("same file from same snapshot returns cached object", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sf1 = project.program.getSourceFile("/src/index.ts"); const sf2 = project.program.getSourceFile("/src/index.ts"); assert.ok(sf1); @@ -1501,11 +1520,11 @@ describe("Source file caching", () => { test("same file from two snapshots (same content) returns cached object", () => { using api = spawnAPI(); - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const snap2 = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const snap2 = api.createSnapshot({ openProject: "/tsconfig.json" }); // Fetch from snap1 first (populates cache), then snap2 (cache hit via hash) - const sf1 = snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); - const sf2 = snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf1 = snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf2 = snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf1); assert.ok(sf2); // Same content hash → cache hit → same object @@ -1516,8 +1535,8 @@ describe("Source file caching", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); using api = disposableAPI; - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const sf1 = snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const sf1 = snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf1); assert.equal(sf1.text, `export const foo = 42;`); @@ -1525,10 +1544,11 @@ describe("Source file caching", () => { fs.writeFile!("/src/foo.ts", `export const foo = 100;`); // Notify the server about the change - const snap2 = api.updateSnapshot({ + const snap2 = api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/foo.ts"] }, }); - const sf2 = snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf2 = snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf2); assert.equal(sf2.text, `export const foo = 100;`); @@ -1540,50 +1560,48 @@ describe("Source file caching", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); using api = disposableAPI; - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const sf1 = snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const sf1 = snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf1); // Mutate a different file fs.writeFile!("/src/foo.ts", `export const foo = 999;`); // Notify the server about the change to foo.ts only - const snap2 = api.updateSnapshot({ + const snap2 = api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/foo.ts"] }, }); - const sf2 = snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf2 = snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf2); // index.ts wasn't changed — should still get cached object assert.strictEqual(sf1, sf2, "Unchanged file should return cached object across snapshots"); }); - test("cache entries survive when one of two snapshots is disposed", () => { + test("disposing the source snapshot releases its cache entries", () => { using api = spawnAPI(); - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); // Fetch from snap1 to populate cache - const sf1 = snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf1 = snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf1); - // snap2 retains snap1's cache refs for unchanged files via snapshot changes - const snap2 = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap2 = api.createSnapshot({ openProject: "/tsconfig.json" }); - // Dispose snap1 — snap2 still holds a ref, so the entry survives snap1.dispose(); - // Fetching from snap2 should still return the cached object - const sf2 = snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); + const sf2 = snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/index.ts"); assert.ok(sf2); - assert.strictEqual(sf1, sf2, "Cache entry should survive when retained by the next snapshot"); + assert.notStrictEqual(sf1, sf2, "independent snapshots do not implicitly retain each other's cache entries"); }); test("invalidateAll causes all files to be re-fetched", () => { const { api: disposableAPI, fs } = spawnAPIWithFS(); using api = disposableAPI; - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const sf1 = snap1.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const sf1 = snap1.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf1); assert.equal(sf1.text, `export const foo = 42;`); @@ -1591,10 +1609,11 @@ describe("Source file caching", () => { fs.writeFile!("/src/foo.ts", `export const foo = "hello";`); // Use invalidateAll to force re-fetch - const snap2 = api.updateSnapshot({ + const snap2 = api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { invalidateAll: true }, }); - const sf2 = snap2.getProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); + const sf2 = snap2.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/foo.ts"); assert.ok(sf2); assert.equal(sf2.text, `export const foo = "hello";`); assert.notStrictEqual(sf1, sf2, "invalidateAll should produce new source file objects"); @@ -1609,8 +1628,8 @@ describe("Source file caching", () => { using api = disposableAPI; // Snapshot 1: get a node and verify getContextualType works - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const proj1 = snap1.getProject("/tsconfig.json")!; + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const proj1 = snap1.getConfiguredProject("/tsconfig.json")!; const sf1 = proj1.program.getSourceFile("/src/main.ts"); assert.ok(sf1); @@ -1628,15 +1647,16 @@ describe("Source file caching", () => { // Snapshot 2: change a different file fs.writeFile!("/src/other.ts", `export const x = 2;`); - const snap2 = api.updateSnapshot({ + const snap2 = api.createSnapshot({ + openProject: "/tsconfig.json", fileChanges: { changed: ["/src/other.ts"] }, }); - const proj2 = snap2.getProject("/tsconfig.json")!; + const proj2 = snap2.getConfiguredProject("/tsconfig.json")!; - // main.ts is unchanged — client returns the cached SourceFile (same object) + // Active snapshots may share a content-addressed source file. const sf2 = proj2.program.getSourceFile("/src/main.ts"); assert.ok(sf2); - assert.strictEqual(sf1, sf2, "unchanged file should be served from client cache"); + assert.strictEqual(sf1, sf2); let numLiteral2: Expression | undefined; sf2.forEachChild(function visit(node) { @@ -1644,10 +1664,10 @@ describe("Source file caching", () => { node.forEachChild(visit); }); assert.ok(numLiteral2, "should find the 42 argument"); - assert.strictEqual(numLiteral, numLiteral2, "unchanged file should be served from client cache"); + assert.strictEqual(numLiteral, numLiteral2); // A type from new snapshot should be resolved - const type2 = proj2.checker.getContextualType(numLiteral); + const type2 = proj2.checker.getContextualType(numLiteral2); assert.ok(type2); assert.ok(type2.flags & TypeFlags.Number); }); @@ -1657,7 +1677,7 @@ describe("Snapshot disposal", () => { test("dispose is idempotent", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); const firstDispose = snapshot.dispose(); const secondDispose = snapshot.dispose(); assert.strictEqual(firstDispose, secondDispose); @@ -1672,7 +1692,7 @@ describe("Snapshot disposal", () => { const api = spawnAPI(); let snapshot: Snapshot; { - using disposableSnapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + using disposableSnapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); snapshot = disposableSnapshot; } assert.ok(snapshot.isDisposed()); @@ -1682,8 +1702,8 @@ describe("Snapshot disposal", () => { test("api.close disposes all active snapshots", () => { const api = spawnAPI(); - const snap1 = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const snap2 = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snap1 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const snap2 = api.createSnapshot({ openProject: "/tsconfig.json" }); assert.ok(!snap1.isDisposed()); assert.ok(!snap2.isDisposed()); api.close(); @@ -1743,14 +1763,13 @@ describe("Source file cache keying across projects", () => { test("different parse modes produce separate cached objects; same parse modes share", () => { using api = spawnAPI(multiProjectFiles); - // Open all three projects - api.updateSnapshot({ openProject: "/projectA/tsconfig.json" }); - api.updateSnapshot({ openProject: "/projectB/tsconfig.json" }); - const snapshot = api.updateSnapshot({ openProject: "/projectC/tsconfig.json" }); + const snapshot = api.createSnapshot({ + openProjects: ["/projectA/tsconfig.json", "/projectB/tsconfig.json", "/projectC/tsconfig.json"], + }); - const projectA = snapshot.getProject("/projectA/tsconfig.json")!; - const projectB = snapshot.getProject("/projectB/tsconfig.json")!; - const projectC = snapshot.getProject("/projectC/tsconfig.json")!; + const projectA = snapshot.getConfiguredProject("/projectA/tsconfig.json")!; + const projectB = snapshot.getConfiguredProject("/projectB/tsconfig.json")!; + const projectC = snapshot.getConfiguredProject("/projectC/tsconfig.json")!; assert.ok(projectA, "projectA should exist"); assert.ok(projectB, "projectB should exist"); assert.ok(projectC, "projectC should exist"); @@ -1782,11 +1801,12 @@ describe("Checker - symbol identity across projects", () => { test("getSymbolAtPosition returns same Symbol instance across projects", () => { using api = spawnAPI(sharedSymbolFiles); - api.updateSnapshot({ openProject: "/projectA/tsconfig.json" }); - const snapshot = api.updateSnapshot({ openProject: "/projectB/tsconfig.json" }); + const snapshot = api.createSnapshot({ + openProjects: ["/projectA/tsconfig.json", "/projectB/tsconfig.json"], + }); - const projectA = snapshot.getProject("/projectA/tsconfig.json")!; - const projectB = snapshot.getProject("/projectB/tsconfig.json")!; + const projectA = snapshot.getConfiguredProject("/projectA/tsconfig.json")!; + const projectB = snapshot.getConfiguredProject("/projectB/tsconfig.json")!; assert.ok(projectA, "projectA should exist"); assert.ok(projectB, "projectB should exist"); @@ -1821,8 +1841,8 @@ export class MyClass { test("getTypeAtPosition", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const xPos = src.indexOf("x = 42"); const type = project.checker.getTypeAtPosition("/src/main.ts", xPos); @@ -1833,8 +1853,8 @@ export class MyClass { test("getTypeAtPosition batched", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const xPos = src.indexOf("x = 42"); const addPos = src.indexOf("add("); @@ -1847,8 +1867,8 @@ export class MyClass { test("getTypeAtLocation", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const firstVarDecl = sourceFile.statements[2]; // "export const x" @@ -1874,8 +1894,8 @@ const c = obj.b.c; using api = spawnAPI(files); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -1920,8 +1940,8 @@ export class Cache { using api = spawnAPI(files); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -1957,8 +1977,8 @@ export class Cache { test("getSignaturesOfType - call signatures", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const addPos = src.indexOf("add("); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", addPos); @@ -1982,8 +2002,8 @@ export class Cache { test("getApparentProperties includes CallableFunction members", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2001,8 +2021,8 @@ export class Cache { collectTiming: true, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2046,8 +2066,8 @@ export class Cache { test("getSignaturesOfType - construct signatures", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const classPos = src.indexOf("MyClass"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", classPos); @@ -2063,8 +2083,8 @@ export class Cache { test("Signature declaration can be resolved", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const addPos = src.indexOf("add("); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", addPos); @@ -2114,8 +2134,8 @@ export class Cache { "/src/main.ts": mainFile, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const liftPos = mainFile.indexOf("lift"); const type = project.checker.getTypeAtPosition("/src/main.ts", liftPos); assert.ok(type); @@ -2134,8 +2154,8 @@ export class Cache { test("Signature.getParameters() returns parameter symbols with correct names", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2154,8 +2174,8 @@ export class Cache { test("Signature.getThisParameter() returns undefined when no explicit this parameter", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2174,8 +2194,8 @@ export class Cache { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("foo(")); assert.ok(symbol); const type = project.checker.getTypeOfSymbol(symbol); @@ -2191,8 +2211,8 @@ export class Cache { test("Signature.getTarget() returns undefined for a non-instantiated signature", () => { using api = spawnAPI(checkerFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = checkerFiles["/src/main.ts"]; const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("add(")); assert.ok(symbol); @@ -2214,8 +2234,8 @@ export class Cache { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let callNode: Node | undefined; @@ -2248,8 +2268,8 @@ export const value = 1; test("getMembers returns class members", () => { using api = spawnAPI(symbolFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = symbolFiles["/src/mod.ts"]; const animalPos = src.indexOf("Animal"); const symbol = project.checker.getSymbolAtPosition("/src/mod.ts", animalPos); @@ -2264,8 +2284,8 @@ export const value = 1; test("getExports returns module exports via sourceFile symbol", () => { using api = spawnAPI(symbolFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/mod.ts"); assert.ok(sourceFile); const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile); @@ -2280,8 +2300,8 @@ export const value = 1; test("getParent returns containing symbol", () => { using api = spawnAPI(symbolFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = symbolFiles["/src/mod.ts"]; const namePos = src.indexOf("name:"); const nameSymbol = project.checker.getSymbolAtPosition("/src/mod.ts", namePos); @@ -2295,8 +2315,8 @@ export const value = 1; test("checkFlags is typed as CheckFlags", () => { using api = spawnAPI(symbolFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/mod.ts", symbolFiles["/src/mod.ts"].indexOf("Animal")); assert.ok(symbol); const checkFlags: CheckFlags = symbol.checkFlags; @@ -2316,8 +2336,8 @@ export const instance: Foo = new Foo(); `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport class Foo {\n x: number = 0;\n}\nexport const instance: Foo = new Foo();\n`; const instancePos = src.indexOf("instance"); const symbol = project.checker.getSymbolAtPosition("/src/types.ts", instancePos); @@ -2347,8 +2367,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; }; function getTypeAtName(api: API, name: string) { - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = typeFiles["/src/types.ts"]; const pos = src.indexOf(name); assert.ok(pos >= 0, `Could not find "${name}" in source`); @@ -2411,8 +2431,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // `string` is neither a union/intersection nor a template literal type, // so it has no constituent types. The client guards on the type's flags @@ -2434,8 +2454,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("IndexType.getTarget() returns the target type", () => { using api = spawnAPI(typeFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.resolveName("KeyOf", SymbolFlags.TypeAlias, { document: "/src/types.ts", position: 0 }); assert.ok(symbol); const type = project.checker.getDeclaredTypeOfSymbol(symbol); @@ -2450,8 +2470,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("IndexedAccessType.getObjectType() and getIndexType()", () => { using api = spawnAPI(typeFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.resolveName("Lookup", SymbolFlags.TypeAlias, { document: "/src/types.ts", position: 0 }); assert.ok(symbol); const type = project.checker.getDeclaredTypeOfSymbol(symbol); @@ -2468,8 +2488,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("ConditionalType.getCheckType() and getExtendsType()", () => { using api = spawnAPI(typeFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.resolveName("Cond", SymbolFlags.TypeAlias, { document: "/src/types.ts", position: 0 }); assert.ok(symbol); const type = project.checker.getDeclaredTypeOfSymbol(symbol); @@ -2486,8 +2506,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("ConditionalType.getTrueType() and getFalseType()", () => { using api = spawnAPI(typeFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.resolveName("Cond", SymbolFlags.TypeAlias, { document: "/src/types.ts", position: 0 }); assert.ok(symbol); const type = project.checker.getDeclaredTypeOfSymbol(symbol); @@ -2521,8 +2541,8 @@ export const tuple: readonly [number, string?, ...boolean[]] = [1]; test("StringMappingType.getTarget() returns the mapped type", () => { using api = spawnAPI(typeFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = typeFiles["/src/types.ts"]; const pos = src.indexOf("Upper"); const symbol = project.checker.getSymbolAtPosition("/src/types.ts", pos); @@ -2551,8 +2571,8 @@ array([]); `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -2603,8 +2623,8 @@ export function gh1449(a: T): T { `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const functionDeclaration = sourceFile.statements.find(isFunctionDeclaration); @@ -2641,8 +2661,8 @@ describe("Checker - intrinsic type getters", () => { test("getAnyType returns a type with Any flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getAnyType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Any); @@ -2651,8 +2671,8 @@ describe("Checker - intrinsic type getters", () => { test("getStringType returns a type with String flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getStringType(); assert.ok(type); assert.ok(type.flags & TypeFlags.String); @@ -2661,8 +2681,8 @@ describe("Checker - intrinsic type getters", () => { test("getNumberType returns a type with Number flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getNumberType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Number); @@ -2671,8 +2691,8 @@ describe("Checker - intrinsic type getters", () => { test("getBooleanType returns a type with Boolean flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getBooleanType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Boolean); @@ -2681,8 +2701,8 @@ describe("Checker - intrinsic type getters", () => { test("getVoidType returns a type with Void flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getVoidType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Void); @@ -2691,8 +2711,8 @@ describe("Checker - intrinsic type getters", () => { test("getUndefinedType returns a type with Undefined flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getUndefinedType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Undefined); @@ -2701,8 +2721,8 @@ describe("Checker - intrinsic type getters", () => { test("getNullType returns a type with Null flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getNullType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Null); @@ -2711,8 +2731,8 @@ describe("Checker - intrinsic type getters", () => { test("getNeverType returns a type with Never flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getNeverType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Never); @@ -2721,8 +2741,8 @@ describe("Checker - intrinsic type getters", () => { test("getUnknownType returns a type with Unknown flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getUnknownType(); assert.ok(type); assert.ok(type.flags & TypeFlags.Unknown); @@ -2731,8 +2751,8 @@ describe("Checker - intrinsic type getters", () => { test("getBigIntType returns a type with BigInt flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getBigIntType(); assert.ok(type); assert.ok(type.flags & TypeFlags.BigInt); @@ -2741,8 +2761,8 @@ describe("Checker - intrinsic type getters", () => { test("getESSymbolType returns a type with ESSymbol flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getESSymbolType(); assert.ok(type); assert.ok(type.flags & TypeFlags.ESSymbol); @@ -2751,8 +2771,8 @@ describe("Checker - intrinsic type getters", () => { test("getNonPrimitiveType returns a type with NonPrimitive flag", () => { using api = spawnAPI(intrinsicFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const type = project.checker.getNonPrimitiveType(); assert.ok(type); assert.ok(type.flags & TypeFlags.NonPrimitive); @@ -2770,14 +2790,13 @@ describe("Checker - multi-project type ID uniqueness", () => { "/proj3/src/index.ts": `export const z = true;`, }); - // Open all 3 projects — each updateSnapshot accumulates open projects - api.updateSnapshot({ openProject: "/proj1/tsconfig.json" }); - api.updateSnapshot({ openProject: "/proj2/tsconfig.json" }); - const snapshot = api.updateSnapshot({ openProject: "/proj3/tsconfig.json" }); + const snapshot = api.createSnapshot({ + openProjects: ["/proj1/tsconfig.json", "/proj2/tsconfig.json", "/proj3/tsconfig.json"], + }); - const proj1 = snapshot.getProject("/proj1/tsconfig.json")!; - const proj2 = snapshot.getProject("/proj2/tsconfig.json")!; - const proj3 = snapshot.getProject("/proj3/tsconfig.json")!; + const proj1 = snapshot.getConfiguredProject("/proj1/tsconfig.json")!; + const proj2 = snapshot.getConfiguredProject("/proj2/tsconfig.json")!; + const proj3 = snapshot.getConfiguredProject("/proj3/tsconfig.json")!; assert.ok(proj1, "proj1 should be in final snapshot"); assert.ok(proj2, "proj2 should be in final snapshot"); assert.ok(proj3, "proj3 should be in final snapshot"); @@ -2823,13 +2842,13 @@ describe("Checker - multi-project type ID uniqueness", () => { "/proj3/src/index.ts": `export function toggle(b: boolean): boolean { return !b; }`, }); - api.updateSnapshot({ openProject: "/proj1/tsconfig.json" }); - api.updateSnapshot({ openProject: "/proj2/tsconfig.json" }); - const snapshot = api.updateSnapshot({ openProject: "/proj3/tsconfig.json" }); + const snapshot = api.createSnapshot({ + openProjects: ["/proj1/tsconfig.json", "/proj2/tsconfig.json", "/proj3/tsconfig.json"], + }); - const proj1 = snapshot.getProject("/proj1/tsconfig.json")!; - const proj2 = snapshot.getProject("/proj2/tsconfig.json")!; - const proj3 = snapshot.getProject("/proj3/tsconfig.json")!; + const proj1 = snapshot.getConfiguredProject("/proj1/tsconfig.json")!; + const proj2 = snapshot.getConfiguredProject("/proj2/tsconfig.json")!; + const proj3 = snapshot.getConfiguredProject("/proj3/tsconfig.json")!; // Get a symbol from each project (exercises symbol registry) const src1 = `export function add(a: number, b: number): number { return a + b; }`; @@ -2873,8 +2892,8 @@ describe("Checker - getBaseTypeOfLiteralType", () => { "/src/main.ts": `export const x = 42;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const x = 42;`; const pos = src.indexOf("x ="); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -2893,8 +2912,8 @@ describe("Checker - getBaseTypeOfLiteralType", () => { "/src/main.ts": `export const s = "hello";`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const s = "hello";`; const pos = src.indexOf("s "); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -2918,8 +2937,8 @@ foo(42); `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -2957,8 +2976,8 @@ export function check(x: string | number) { `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport function check(x: string | number) {\n if (typeof x === "string") {\n return x;\n }\n return x;\n}\n`; // Get the symbol for parameter "x" @@ -3008,8 +3027,8 @@ export const obj = { name }; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -3057,8 +3076,8 @@ describe("readFile callback semantics", () => { fs, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // 1. String content: virtual file is found const sf = project.program.getSourceFile("/src/index.ts"); @@ -3202,7 +3221,7 @@ describe("updateSnapshot file systems", () => { fs, }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: { kind: "full", @@ -3216,7 +3235,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.equal(sourceFile?.text, `export const source = "memory";`); assert.equal(project.program.getSourceFile("/host.ts"), undefined); @@ -3227,14 +3246,14 @@ describe("updateSnapshot file systems", () => { using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystemWithLib(Object.entries({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true }, files: ["src/main.ts"] }), "/src/main.ts": `export const values: Array = [];`, })), }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; assert.deepEqual(program.getGlobalDiagnostics(), []); const sourceFileNames = program.getSourceFileNames(); const defaultLibraryName = sourceFileNames.find(fileName => fileName.includes("/lib.") && fileName.endsWith(".d.ts")); @@ -3251,7 +3270,7 @@ describe("updateSnapshot file systems", () => { using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openFiles: [fileDocument, remoteDocument, notebookDocument], fileSystem: createFileSystem([ [fileDocument, `export const file = true;`], @@ -3289,7 +3308,7 @@ describe("updateSnapshot file systems", () => { fs, }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: { kind: "layer", @@ -3303,7 +3322,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/tsconfig.json")!; assert.equal((project.program.getSourceFile("/src/index.ts"))?.text, `export const cached = true;`); assert.equal((project.program.getSourceFile("/src/fallback.ts"))?.text, `export const fallback = true;`); @@ -3322,14 +3341,14 @@ describe("updateSnapshot file systems", () => { }), }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystemLayer([ ["/tsconfig.json", JSON.stringify({ compilerOptions: { noLib: true }, include: ["src/**/*.ts"] })], ["/src/from-cache.ts", `export const cache = true;`], ]), }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; assert.deepEqual( [...program.getSourceFileNames()].sort(), ["/src/from-cache.ts", "/src/from-host.ts"], @@ -3348,7 +3367,7 @@ describe("updateSnapshot file systems", () => { }, }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/project/tsconfig.json", fileSystem: { kind: "full", @@ -3362,7 +3381,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/project/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/project/tsconfig.json")!; assert.equal( (project.program.getSourceFile("/packages/pkg/index.d.ts"))?.text, `export declare const value: number;`, @@ -3382,7 +3401,7 @@ describe("updateSnapshot file systems", () => { }, }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/project/tsconfig.json", fileSystem: { kind: "full", @@ -3396,7 +3415,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/project/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/project/tsconfig.json")!; assert.equal( (project.program.getSourceFile("/project/pkg/index.d.ts"))?.text, `export declare const value: number;`, @@ -3408,7 +3427,7 @@ describe("updateSnapshot file systems", () => { using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystem(Object.entries({ "/tsconfig.json": JSON.stringify({ @@ -3423,6 +3442,7 @@ describe("updateSnapshot file systems", () => { }); using updated = snapshot.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer( Object.entries({ "/src/change.ts": `export const version = "new";`, @@ -3433,15 +3453,17 @@ describe("updateSnapshot file systems", () => { }, ), }); - const project = updated.getProject("/tsconfig.json")!; + const project = updated.getConfiguredProject("/tsconfig.json")!; assert.equal((project.program.getSourceFile("/src/keep.ts"))?.text, `export const keep = true;`); assert.equal((project.program.getSourceFile("/src/change.ts"))?.text, `export const version = "new";`); assert.equal((project.program.getSourceFile("/src/added.ts"))?.text, `export const added = true;`); assert.equal(project.program.getSourceFile("/src/remove.ts"), undefined); assert.equal(project.program.getSourceFile("/src/removed/gone.ts"), undefined); - assert.throws(() => snapshot.update(), /can only update the latest snapshot/); + using fork = snapshot.update({ ensurePrograms: true }); + assert.equal((fork.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/change.ts"))?.text, `export const version = "old";`); using updatedAgain = updated.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer( Object.entries({ "/src/added.ts": `export const added = "updated again";`, @@ -3451,7 +3473,7 @@ describe("updateSnapshot file systems", () => { }, ), }); - const updatedAgainProject = updatedAgain.getProject("/tsconfig.json")!; + const updatedAgainProject = updatedAgain.getConfiguredProject("/tsconfig.json")!; assert.equal((updatedAgainProject.program.getSourceFile("/src/keep.ts"))?.text, `export const keep = true;`); assert.equal((updatedAgainProject.program.getSourceFile("/src/added.ts"))?.text, `export const added = "updated again";`); assert.equal(updatedAgainProject.program.getSourceFile("/src/change.ts"), undefined); @@ -3461,7 +3483,7 @@ describe("updateSnapshot file systems", () => { using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - let snapshot: Snapshot = api.updateSnapshot({ + let snapshot: Snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystem([ ["/tsconfig.json", JSON.stringify({ compilerOptions: { noLib: true }, files: ["pkg/index.ts"] })], @@ -3474,13 +3496,14 @@ describe("updateSnapshot file systems", () => { const oldSnapshot: Snapshot = snapshot; content += character; snapshot = oldSnapshot.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer([["/pkg/index.ts", content]]), }); oldSnapshot.dispose(); assert.equal(oldSnapshot.isDisposed(), true); } - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; assert.equal((program.getSourceFile("/pkg/index.ts"))?.text, "export const x = 1"); } finally { @@ -3496,15 +3519,16 @@ describe("updateSnapshot file systems", () => { cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), fs: host, }); - using snapshot = api.updateSnapshot(); + using snapshot = api.createSnapshot(); using replaced = snapshot.update({ + ensurePrograms: true, openProject: "/tsconfig.json", fileSystem: createFileSystem([ ["/tsconfig.json", JSON.stringify({ compilerOptions: { noLib: true }, files: ["memory.ts", "host.ts"] })], ["/memory.ts", `export const source = "memory";`], ]), }); - const program = replaced.getProject("/tsconfig.json")!.program; + const program = replaced.getConfiguredProject("/tsconfig.json")!.program; assert.equal((program.getSourceFile("/memory.ts"))?.text, `export const source = "memory";`); assert.equal(program.getSourceFile("/host.ts"), undefined); }); @@ -3513,7 +3537,7 @@ describe("updateSnapshot file systems", () => { using api = new API({ cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystem( Object.entries({ @@ -3531,6 +3555,7 @@ describe("updateSnapshot file systems", () => { }); using updated = snapshot.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer( Object.entries({ "/target/change.ts": `export const version = "new";`, @@ -3541,7 +3566,7 @@ describe("updateSnapshot file systems", () => { }, ), }); - const program = updated.getProject("/tsconfig.json")!.program; + const program = updated.getConfiguredProject("/tsconfig.json")!.program; assert.equal((program.getSourceFile("/src/link/change.ts"))?.text, `export const version = "new";`); assert.equal((program.getSourceFile("/src/link/added.ts"))?.text, `export const added = true;`); assert.equal(program.getSourceFile("/src/link/remove.ts"), undefined); @@ -3557,14 +3582,14 @@ describe("updateSnapshot file systems", () => { }, }, }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystem(Object.entries({ "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, outDir: "/out", rootDir: "/src" }, files: ["src/main.ts"] }), "/src/main.ts": `export const value: number = 1;`, })), }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const result = program.emit(); assert.deepEqual(result.emittedFiles, ["/out/main.js"]); assert.deepEqual(result.fileSystem, { @@ -3577,7 +3602,7 @@ describe("updateSnapshot file systems", () => { using updated = snapshot.update({ fileSystem: result.fileSystem!, openFiles: ["/out/main.js"] }); const outputProject = updated.getDefaultProjectForFile("/out/main.js"); - assert.equal((updated.getProject("/tsconfig.json")!.program.getSourceFile("/src/main.ts"))?.text, `export const value: number = 1;`); + assert.equal((updated.getConfiguredProject("/tsconfig.json")!.program.getSourceFile("/src/main.ts"))?.text, `export const value: number = 1;`); assert.equal((outputProject?.program.getSourceFile("/out/main.js"))?.text, `export const value = 1;\n`); }); @@ -3587,14 +3612,14 @@ describe("updateSnapshot file systems", () => { cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), fs: host, }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/tsconfig.json", fileSystem: createFileSystemLayer(Object.entries({ "/tsconfig.json": JSON.stringify({ compilerOptions: { noLib: true, outDir: "/out", rootDir: "/src" }, files: ["src/main.ts"] }), "/src/main.ts": `export const value: number = 1;`, })), }); - const program = snapshot.getProject("/tsconfig.json")!.program; + const program = snapshot.getConfiguredProject("/tsconfig.json")!.program; const result = program.emit(); assert.equal(result.fileSystem, undefined); assert.equal(host.readFile!("/out/main.js"), `export const value = 1;\n`); @@ -3627,7 +3652,7 @@ describe("updateSnapshot file systems", () => { }, }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/project/tsconfig.json", fileSystem: { kind: "full", @@ -3640,7 +3665,7 @@ describe("updateSnapshot file systems", () => { }, }, }); - const project = snapshot.getProject("/project/tsconfig.json")!; + const project = snapshot.getConfiguredProject("/project/tsconfig.json")!; const sourceFileNames = project.program.getSourceFileNames(); assert.ok( sourceFileNames.includes("/host/node_modules/pkg/index.d.ts"), @@ -3662,7 +3687,7 @@ describe("updateSnapshot file systems", () => { cwd: fileURLToPath(new URL("../../../../", import.meta.url).toString()), fs: host, }); - using snapshot = api.updateSnapshot({ + using snapshot = api.createSnapshot({ openProject: "/project/tsconfig.json", fileSystem: createFileSystem([ ["/project/tsconfig.json", JSON.stringify({ compilerOptions: { noLib: true, moduleResolution: "node" }, files: ["index.ts"] })], @@ -3670,13 +3695,14 @@ describe("updateSnapshot file systems", () => { ]), }); using updated = snapshot.update({ + ensurePrograms: true, fileSystem: createFileSystemLayer([], { symlinks: { "/project/node_modules": { target: "/host/node_modules", host: true }, }, }), }); - const project = updated.getProject("/project/tsconfig.json")!; + const project = updated.getConfiguredProject("/project/tsconfig.json")!; assert.equal( (project.program.getSourceFile("/host/node_modules/pkg/index.d.ts"))?.text, `export declare const value: string;`, @@ -3697,8 +3723,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const xs: number[] = [];`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const xs: number[] = [];`; const pos = src.indexOf("xs"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3715,8 +3741,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const xs: readonly number[] = [];`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const xs: readonly number[] = [];`; const pos = src.indexOf("xs"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3733,8 +3759,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const xs: Array = [];`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const xs: Array = [];`; const pos = src.indexOf("xs"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3751,8 +3777,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const tup: [number, string] = [1, "a"];`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const tup: [number, string] = [1, "a"];`; const pos = src.indexOf("tup"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3769,8 +3795,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const tup: readonly [number, string] = [1, "a"];`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const tup: readonly [number, string] = [1, "a"];`; const pos = src.indexOf("tup"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3787,8 +3813,8 @@ describe("Checker - isArrayType / isTupleType", () => { "/src/main.ts": `export const str: string = "";`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const str: string = "";`; const pos = src.indexOf("str"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3814,8 +3840,8 @@ export type ReadonlyUser = Readonly; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const user = sourceFile.statements.find(isInterfaceDeclaration); @@ -3840,8 +3866,8 @@ export type ReadonlyUser = Readonly; "/src/main.ts": `export const a = 1; export let b = 2;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker } = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getConfiguredProject("/tsconfig.json")!; const a = checker.getSymbolAtPosition("/src/main.ts", "export const ".length); const b = checker.getSymbolAtPosition("/src/main.ts", "export const a = 1; export let ".length); assert.ok(a); @@ -3874,8 +3900,8 @@ export type B = InstanceType; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const typeAliases = sourceFile.statements.filter(isTypeAliasDeclaration); @@ -3898,8 +3924,8 @@ describe("Checker - getReturnTypeOfSignature", () => { "/src/main.ts": `export function add(a: number, b: number): number { return a + b; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function add(a: number, b: number): number { return a + b; }`; const pos = src.indexOf("add("); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3921,8 +3947,8 @@ describe("Checker - getRestTypeOfSignature", () => { "/src/main.ts": `export function sum(...nums: number[]): number { return nums.reduce((a, b) => a + b, 0); }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function sum(...nums: number[]): number { return nums.reduce((a, b) => a + b, 0); }`; const pos = src.indexOf("sum("); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3944,8 +3970,8 @@ describe("Checker - getTypePredicateOfSignature", () => { "/src/main.ts": `export function isString(x: unknown): x is string { return typeof x === "string"; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function isString(x: unknown): x is string { return typeof x === "string"; }`; const pos = src.indexOf("isString("); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3976,8 +4002,8 @@ export class Dog extends Animal { `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport class Animal {\n isdog(): this is Dog { return this instanceof Dog; }\n}\nexport class Dog extends Animal {\n bark() {}\n}\n`; const pos = src.indexOf("isdog("); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -3997,8 +4023,8 @@ export class Dog extends Animal { "/src/main.ts": `export function assertIsString(x: unknown): asserts x is string { if (typeof x !== "string") throw new Error(); }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function assertIsString(x: unknown): asserts x is string { if (typeof x !== "string") throw new Error(); }`; const pos = src.indexOf("assertIsString("); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4022,8 +4048,8 @@ export class Dog extends Animal { "/src/main.ts": `export function add(a: number, b: number): number { return a + b; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function add(a: number, b: number): number { return a + b; }`; const pos = src.indexOf("add("); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4051,8 +4077,8 @@ export class Derived extends Base { `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport class Base {\n x: number = 0;\n}\nexport class Derived extends Base {\n y: string = "";\n}\n`; const pos = src.indexOf("Derived"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4079,8 +4105,8 @@ export interface Dog extends Animal { `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface Animal {\n name: string;\n}\nexport interface Dog extends Animal {\n bark(): void;\n}\n`; const pos = src.indexOf("Dog"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4105,8 +4131,8 @@ export type BoxOfString = Box; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const typeAlias = sourceFile.statements.find(isTypeAliasDeclaration); @@ -4135,8 +4161,8 @@ export const n: number = 0; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport class Base {\n x: number = 0;\n}\nexport class Derived extends Base {\n y: string = "";\n}\nexport const n: number = 0;\n`; const derivedSymbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("Derived")); @@ -4168,8 +4194,8 @@ declare const bad: ThisTypeDoesNotExist; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\ndeclare const good: string;\ndeclare const bad: ThisTypeDoesNotExist;\n`; const badSymbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("bad")); @@ -4198,8 +4224,8 @@ export type Alias = typeof value; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport const value = 1;\nexport type Alias = typeof value;\n`; // A real symbol is not the unknown/undefined symbol. @@ -4223,8 +4249,8 @@ notCallable(); "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const calls: Node[] = []; @@ -4257,8 +4283,8 @@ export const value = 1; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile); @@ -4285,8 +4311,8 @@ export const obj: { a: number } = { a: 1 }; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport const obj: { a: number } = { a: 1 };\n`; const objSymbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("obj")); assert.ok(objSymbol); @@ -4318,8 +4344,8 @@ export { x as '${maliciousName}' }; "/src/main.ts": source, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const typesFile = project.program.getSourceFile("/src/types.d.ts"); assert.ok(typesFile); @@ -4392,8 +4418,8 @@ export const total = add(1, 2); `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const functions = [...sourceFile.statements].filter(isFunctionDeclaration); @@ -4454,8 +4480,8 @@ var measure = function (name) { `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.js"); assert.ok(sourceFile); const variable = sourceFile.statements.find(isVariableStatement); @@ -4489,8 +4515,8 @@ const cast = /** @type {number} */ (someValue); `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.js"); assert.ok(sourceFile); const statements = [...sourceFile.statements].filter(isVariableStatement); @@ -4520,8 +4546,8 @@ export declare const p: Person; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface Person {\n name: string;\n age: number;\n greet(): void;\n}\nexport declare const p: Person;\n`; const pos = src.indexOf("p: Person"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4549,8 +4575,8 @@ export declare const m: StringMap; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface StringMap {\n [key: string]: number;\n}\nexport declare const m: StringMap;\n`; const pos = src.indexOf("m: StringMap"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4578,8 +4604,8 @@ export declare const m: ReadonlyMap; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface ReadonlyMap {\n readonly [key: string]: number;\n}\nexport declare const m: ReadonlyMap;\n`; const pos = src.indexOf("m: ReadonlyMap"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4599,8 +4625,8 @@ describe("Checker - getConstraintOfTypeParameter", () => { "/src/main.ts": `export function identity(x: T): T { return x; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function identity(x: T): T { return x; }`; const pos = src.indexOf("identity<"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4624,8 +4650,8 @@ describe("Checker - TypeParameter getters", () => { "/src/main.ts": `export function f(x: T): T { return x; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function f(x: T): T { return x; }`; const pos = src.indexOf("f<"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4653,8 +4679,8 @@ describe("Checker - TypeParameter getters", () => { "/src/main.ts": `export function f(x: T): T { return x; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function f(x: T): T { return x; }`; const pos = src.indexOf("f<"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4679,8 +4705,8 @@ describe("Checker - getTypeArguments", () => { "/src/main.ts": `export const arr: Array = [1, 2, 3];`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export const arr: Array = [1, 2, 3];`; const pos = src.indexOf("arr:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4699,8 +4725,8 @@ describe("Checker - getTypeArguments", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // `string` is not a type reference. When getTypeArguments is reached // with one, the server panics, but the per-request panic recovery @@ -4728,8 +4754,8 @@ describe("Checker - getBaseConstraintOfType", () => { "/src/main.ts": `export function identity(x: T): T { return x; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `export function identity(x: T): T { return x; }`; const pos = src.indexOf("identity<"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4750,8 +4776,8 @@ describe("Checker - getBaseConstraintOfType", () => { "/src/main.ts": `export const x: number = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = `export const x: number = 1;`.indexOf("x:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -4775,8 +4801,8 @@ export declare const p: Person; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const src = `\nexport interface Person {\n name: string;\n age: number;\n}\nexport declare const p: Person;\n`; const pos = src.indexOf("p: Person"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); @@ -4798,8 +4824,8 @@ describe("Checker - getConstantValue", () => { "/src/main.ts": `export enum E { A = 1, B = 2 }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let memberB: Node | undefined; @@ -4821,8 +4847,8 @@ describe("Checker - getConstantValue", () => { "/src/main.ts": `export enum Color { Red = "red" }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let member: Node | undefined; @@ -4843,8 +4869,8 @@ describe("Checker - getSignatureFromDeclaration", () => { "/src/main.ts": `export function add(a: number, b: number): number { return a + b; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let funcDecl: Node | undefined; @@ -4872,8 +4898,8 @@ export { value as renamed }; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); let exportSpecifier: Node | undefined; @@ -4896,8 +4922,8 @@ describe("Checker - getAliasedSymbol", () => { "/src/main.ts": `import { foo } from "./foo";\nexport const usage = foo;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = `import { foo } from "./foo";`.indexOf("foo }"); const aliasSymbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(aliasSymbol); @@ -4921,8 +4947,8 @@ export class Standalone {} `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile); @@ -4950,8 +4976,8 @@ export * from "./inner"; `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); const moduleSymbol = project.checker.getSymbolAtLocation(sourceFile); @@ -4979,8 +5005,8 @@ function f() { test("returns symbols visible at a position", () => { using api = spawnAPI(scopeFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = scopeFiles["/src/main.ts"].indexOf("return innerValue"); const symbols = project.checker.getSymbolsInScope( { document: "/src/main.ts", position: pos }, @@ -4996,8 +5022,8 @@ function f() { test("returns type symbols when asked for type meaning at a node", () => { using api = spawnAPI(scopeFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); const symbols = project.checker.getSymbolsInScope(sourceFile, SymbolFlags.Type); @@ -5011,8 +5037,8 @@ function f() { test("SymbolFlags.All includes both value and type meanings", () => { using api = spawnAPI(scopeFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = scopeFiles["/src/main.ts"].indexOf("return innerValue"); const symbols = project.checker.getSymbolsInScope( { document: "/src/main.ts", position: pos }, @@ -5040,8 +5066,8 @@ export function add(a: number, b: number): number { return a + b; } test("getDocumentationComment returns the leading comment text", () => { using api = spawnAPI(docFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = docFiles["/src/main.ts"].indexOf("add(a"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5053,8 +5079,8 @@ export function add(a: number, b: number): number { return a + b; } test("getJsDocTags returns structured tag name/text pairs", () => { using api = spawnAPI(docFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = docFiles["/src/main.ts"].indexOf("add(a"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5076,8 +5102,8 @@ describe("TypeParameter - isThisType", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // ": this {" — offset 2 past ': ' lands on 't' in the return-type 'this' const pos = src.indexOf(": this {") + 2; const type = project.checker.getTypeAtPosition("/src/main.ts", pos); @@ -5094,8 +5120,8 @@ describe("TypeParameter - isThisType", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // Point to 'T' in the type parameter declaration '' — getTypeAtPosition // on a type annotation reference doesn't resolve to TypeParameter, but // the declaration position does. @@ -5116,8 +5142,8 @@ describe("Type - getAliasTypeArguments", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("x:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5135,8 +5161,8 @@ describe("Type - getAliasTypeArguments", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("p:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5155,8 +5181,8 @@ describe("Type - getAliasTypeArguments", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("arr:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5176,8 +5202,8 @@ describe("Type - getAliasSymbol", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("p:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5195,8 +5221,8 @@ describe("Type - getAliasSymbol", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("c:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5214,8 +5240,8 @@ describe("Type - getAliasSymbol", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("str:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5234,8 +5260,8 @@ describe("IntrinsicType - intrinsicName", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const stringType = project.checker.getStringType(); assert.equal((stringType as IntrinsicType).intrinsicName, "string"); const anyType = project.checker.getAnyType(); @@ -5260,8 +5286,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const symbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("empty:")); assert.ok(symbol); const type = project.checker.getTypeOfSymbol(symbol); @@ -5278,8 +5304,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("greeting:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5297,8 +5323,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const posSymbol = project.checker.getSymbolAtPosition("/src/main.ts", src.indexOf("pos =")); assert.ok(posSymbol); @@ -5326,8 +5352,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("greeting:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5354,8 +5380,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("greeting:"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5381,8 +5407,8 @@ describe("FreshableType - getFreshType and getRegularType", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("Pending"); const symbol = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(symbol); @@ -5413,8 +5439,8 @@ describe("Checker - isContextSensitive", () => { "/src/main.ts": `export const fn = (x) => x;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); // Find the arrow function node @@ -5438,8 +5464,8 @@ describe("Checker - isTypeAssignableTo", () => { "/src/main.ts": `export {};`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const stringType = project.checker.getStringType(); const anyType = project.checker.getAnyType(); const neverType = project.checker.getNeverType(); @@ -5454,8 +5480,8 @@ describe("Checker - isTypeAssignableTo", () => { "/src/main.ts": `export {};`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const stringType = project.checker.getStringType(); const numberType = project.checker.getNumberType(); assert.ok(!project.checker.isTypeAssignableTo(numberType, stringType), "number not assignable to string"); @@ -5469,8 +5495,8 @@ describe("Checker - isTypeAssignableTo", () => { "/src/main.ts": src, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const pos = src.indexOf("x:"); const sym = project.checker.getSymbolAtPosition("/src/main.ts", pos); assert.ok(sym); @@ -5498,8 +5524,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created keyword type", () => { using api = spawnAPI(emitterFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createKeywordTypeNode(SyntaxKind.StringKeyword); const text = project.emitter.printNode(node); assert.strictEqual(text, "string"); @@ -5508,8 +5534,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created union type", () => { using api = spawnAPI(emitterFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createUnionTypeNode([ createKeywordTypeNode(SyntaxKind.StringKeyword), createKeywordTypeNode(SyntaxKind.NumberKeyword), @@ -5521,8 +5547,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created function type", () => { using api = spawnAPI(emitterFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const param = createParameterDeclaration( undefined, undefined, @@ -5543,8 +5569,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created type reference", () => { using api = spawnAPI(emitterFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createTypeReferenceNode(createIdentifier("Array"), [ createKeywordTypeNode(SyntaxKind.StringKeyword), ]); @@ -5555,8 +5581,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("printNode with factory-created array type", () => { using api = spawnAPI(emitterFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createArrayTypeNode(createKeywordTypeNode(SyntaxKind.NumberKeyword)); const text = project.emitter.printNode(node); assert.strictEqual(text, "number[]"); @@ -5565,8 +5591,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("typeToTypeNode + printNode round-trip", () => { using api = spawnAPI(emitterFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker, emitter } = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker, emitter } = snapshot.getConfiguredProject("/tsconfig.json")!; const src = emitterFiles["/src/main.ts"]; const greetPos = src.indexOf("greet("); @@ -5584,8 +5610,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("visitEachChild on typeToTypeNode result with keyword types", () => { using api = spawnAPI(emitterFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker } = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getConfiguredProject("/tsconfig.json")!; const src = emitterFiles["/src/main.ts"]; const objPos = src.indexOf("obj"); const symbol = checker.getSymbolAtPosition("/src/main.ts", objPos); @@ -5632,8 +5658,8 @@ export const obj = { m: 1, s: "hi", b: true }; test("typeToString", () => { using api = spawnAPI(emitterFiles); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker } = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getConfiguredProject("/tsconfig.json")!; const src = emitterFiles["/src/main.ts"]; const greetPos = src.indexOf("greet("); @@ -5651,8 +5677,8 @@ export const obj = { m: 1, s: "hi", b: true }; "/src/main.ts": `export function greet(name: string): string[] { return [name]; }`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const { checker } = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const { checker } = snapshot.getConfiguredProject("/tsconfig.json")!; const greetPos = "export function greet".indexOf("greet"); const symbol = checker.getSymbolAtPosition("/src/main.ts", greetPos); assert.ok(symbol); @@ -5668,8 +5694,8 @@ export const obj = { m: 1, s: "hi", b: true }; "/src/main.ts": `const foo = /asdfasf;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/main.ts"); assert.ok(sourceFile); @@ -5713,8 +5739,8 @@ describe("Program - selected file emit", () => { const { api: disposableAPI, fs } = spawnAPIWithFS({ ...files }); using api = disposableAPI; - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = project.program.getJavaScriptEmit(["/src/a.ts", "/src/b.ts"]); assert.equal(result.emitSkipped, false); assert.deepEqual([...result.outputFiles.keys()], [ @@ -5732,8 +5758,8 @@ describe("Program - selected file emit", () => { const { api: disposableAPI, fs } = spawnAPIWithFS({ ...files }); using api = disposableAPI; - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = project.program.getDeclarationEmit(["/src/a.ts", "/src/b.ts"]); assert.equal(result.emitSkipped, false); assert.deepEqual([...result.outputFiles.keys()], [ @@ -5749,8 +5775,8 @@ describe("Program - selected file emit", () => { test("selected file emit accepts empty arrays", () => { using api = spawnAPI({ ...files }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; assert.deepEqual((project.program.getJavaScriptEmit([])).outputFiles, new Map()); assert.deepEqual((project.program.getDeclarationEmit([])).outputFiles, new Map()); }); @@ -5764,7 +5790,7 @@ describe("SnapshotInternalAPI - formatNodeForInsertion", () => { }; using api = spawnAPI(files); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); const node = createVariableStatement( undefined, @@ -5789,7 +5815,7 @@ describe("SnapshotInternalAPI - formatNodeForInsertion", () => { }; using api = spawnAPI(files); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); const node = createVariableStatement( undefined, @@ -5813,7 +5839,7 @@ describe("SnapshotInternalAPI - formatNodeForInsertion", () => { }; using api = spawnAPI(files); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); const node = createVariableStatement( undefined, @@ -5841,8 +5867,8 @@ describe("modifierFlags", () => { "/src/index.ts": `export async function foo() {}`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -5865,8 +5891,8 @@ describe("modifierFlags", () => { "/src/index.ts": `function bar() {}`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -5889,8 +5915,8 @@ describe("Checker - getResolvedSymbol", () => { "/src/index.ts": `const x = 1;\nconst y = x;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -5918,8 +5944,8 @@ describe("VariableDeclarationList - BlockScoped flags", () => { "/src/index.ts": `let x = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -5940,8 +5966,8 @@ describe("VariableDeclarationList - BlockScoped flags", () => { "/src/index.ts": `const x = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -5963,8 +5989,8 @@ test("TypeOperator operator kind", () => { "/src/index.ts": `function test(arg: readonly number[]) { }\n`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert(sourceFile); const param = (sourceFile.statements[0] as import("@typescript/typescript/unstable/ast").FunctionDeclaration).parameters[0]; @@ -5983,8 +6009,8 @@ test("SpreadAssignment roundtrip", () => { "/src/index.ts": `var thing = { ...other };\n`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert(sourceFile); const stmt = sourceFile.statements[0] as import("@typescript/typescript/unstable/ast").VariableStatement; @@ -6005,8 +6031,8 @@ test("VariableDeclarationList const flag clone", () => { "/src/index.ts": `const thing = 123;\n`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert(sourceFile); { @@ -6035,8 +6061,8 @@ doThing(); `, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert(sourceFile); const printed = project.emitter.printNode(sourceFile); @@ -6046,8 +6072,8 @@ doThing(); test("Factory ModifierList auto-conversion", () => { using api = spawnAPI(); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const node = createTypeAliasDeclaration( [createToken(SyntaxKind.ExportKeyword)], createIdentifier("Test"), @@ -6074,8 +6100,8 @@ test("Parse-clone-emit roundtrip", () => { const errors = { ...target }; for (const tsconfig of globSync("**/tsconfig.json", { cwd: tsSource })) { - const snapshot = api.updateSnapshot({ openProject: resolve(tsSource, tsconfig) }); - const project = snapshot.getProject(tsconfig); + const snapshot = api.createSnapshot({ openProject: resolve(tsSource, tsconfig) }); + const project = snapshot.getConfiguredProject(tsconfig); assert(project); for (const file of project.rootFiles) { const source = project.program.getSourceFile(file); @@ -6119,8 +6145,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": source, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getSyntacticDiagnostics("/src/index.ts"); assert.deepEqual(diags[0].startPosition, { line: 0, character: 9 }); assert.deepEqual(diags[0].endPosition, { line: 0, character: 10 }); @@ -6141,8 +6167,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": source, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getSemanticDiagnostics("/src/index.ts"); const declRange = rangeOf(source, "callback", 0); const assignRange = rangeOf(source, "callback", 1); @@ -6183,8 +6209,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": source, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getSuggestionDiagnostics("/src/index.ts"); assert.deepEqual(withoutFormattingContext(diags), [{ fileName: "/src/index.ts", @@ -6203,8 +6229,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getConfigFileParsingDiagnostics(); assert.deepEqual(withoutFormattingContext(diags), [{ fileName: "/tsconfig.json", @@ -6224,8 +6250,8 @@ describe("Program - diagnostics", () => { }); using api = disposableAPI; - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const names = project.program.getConfigFileNames(); assert.deepEqual(names, ["/tsconfig.json", "/tsconfig.base.json"]); @@ -6250,8 +6276,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x: number = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getDeclarationDiagnostics("/src/index.ts"); assert.deepEqual(diags, []); }); @@ -6263,8 +6289,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": source, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getBindDiagnostics("/src/index.ts"); assert.deepEqual(withoutFormattingContext(diags), [ { @@ -6291,8 +6317,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getProgramDiagnostics(); assert.deepEqual(withoutFormattingContext(diags), [ { @@ -6318,8 +6344,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getGlobalDiagnostics(); assert.deepEqual(diags, []); }); @@ -6330,8 +6356,8 @@ describe("Program - diagnostics", () => { "/src/index.ts": `export const x = [1, 2, 3];`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getGlobalDiagnostics(); // With noLib, the checker reports "Cannot find global type" diagnostics that // are not associated with any source file. @@ -6357,8 +6383,8 @@ describe("Program - diagnostics", () => { "/src/clean.ts": `const c = 3;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getSyntacticDiagnostics(["/src/a.ts", "/src/b.ts"]); assert.deepEqual(withoutFormattingContext(diags), [ { @@ -6387,8 +6413,8 @@ describe("Program - diagnostics", () => { "/src/b.ts": sourceB, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getSemanticDiagnostics(["/src/a.ts", "/src/b.ts"]); assert.equal(diags.length, 2); assert.equal(diags[0].fileName, "/src/a.ts"); @@ -6406,8 +6432,8 @@ describe("Program - diagnostics", () => { "/src/b.ts": sourceB, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getBindDiagnostics(["/src/a.ts", "/src/b.ts"]); assert.equal(diags.length, 4); assert.equal(diags.filter(d => d.fileName === "/src/a.ts").length, 2); @@ -6421,8 +6447,8 @@ describe("Program - diagnostics", () => { "/src/b.ts": `const b: = 2;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getSyntacticDiagnostics(); assert.equal(diags.length, 2); }); @@ -6434,8 +6460,8 @@ describe("Program - diagnostics", () => { "/src/b.ts": `const b: = 2;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const diags = project.program.getSyntacticDiagnostics([]); assert.deepEqual(diags, []); }); @@ -6450,8 +6476,8 @@ describe("getDefaultProjectForFile", () => { "/node_modules/my-lib/index.d.ts": `export declare const foo: string;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // The d.ts is not imported, so it is not in the project's program const dtsSf = project.program.getSourceFile("/node_modules/my-lib/index.d.ts"); @@ -6461,8 +6487,8 @@ describe("getDefaultProjectForFile", () => { const noProject = snapshot.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"); assert.equal(noProject, undefined, "getDefaultProjectForFile returns undefined for unloaded file"); - // Load the file into the inferred project via updateSnapshot openFiles - const snapshot2 = api.updateSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); + // Load the file into the inferred project via createSnapshot openFiles + const snapshot2 = api.createSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); const defaultProject = snapshot2.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"); assert.ok(defaultProject, "getDefaultProjectForFile should find inferred project after openFiles"); @@ -6478,7 +6504,7 @@ describe("getDefaultProjectForFile", () => { }); using api = disposableAPI; - const snapshot1 = api.updateSnapshot({ openFiles: ["/loose.ts"] }); + const snapshot1 = api.createSnapshot({ openFiles: ["/loose.ts"] }); const project1 = snapshot1.getDefaultProjectForFile("/loose.ts"); assert.ok(project1, "file with no config file in its ancestry should load into the inferred project"); const sf1 = project1.program.getSourceFile("/loose.ts"); @@ -6487,7 +6513,8 @@ describe("getDefaultProjectForFile", () => { // Mutate the file and notify only via fileChanges — no follow-up openFiles/closeFiles. fs.writeFile!("/loose.ts", `export const foo = 2;`); - const snapshot2 = api.updateSnapshot({ + const snapshot2 = api.createSnapshot({ + openFiles: ["/loose.ts"], fileChanges: { changed: ["/loose.ts"] }, }); @@ -6502,7 +6529,7 @@ describe("getDefaultProjectForFile", () => { ); }); - test("keeps previously opened files open across subsequent openFiles calls", () => { + test("opens multiple inferred files in one snapshot", () => { using api = spawnAPI({ "/tsconfig.json": JSON.stringify({ compilerOptions: { strict: true } }), "/src/index.ts": `export const x = 1;`, @@ -6512,11 +6539,9 @@ describe("getDefaultProjectForFile", () => { "/node_modules/other-lib/index.d.ts": `export declare const bar: number;`, }); - api.updateSnapshot({ openProject: "/tsconfig.json" }); - api.updateSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); - - // Opening a second file in a later snapshot must not close the first one. - const snapshot = api.updateSnapshot({ openFiles: ["/node_modules/other-lib/index.d.ts"] }); + const snapshot = api.createSnapshot({ + openFiles: ["/node_modules/my-lib/index.d.ts", "/node_modules/other-lib/index.d.ts"], + }); const firstProject = snapshot.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"); assert.ok(firstProject, "previously opened file should remain in the inferred project"); @@ -6532,7 +6557,7 @@ describe("getDefaultProjectForFile", () => { // Open the file without first opening the project. Like LSP's didOpen, this // should search ancestor directories for a tsconfig that contains the file. - const snapshot = api.updateSnapshot({ openFiles: ["/src/index.ts"] }); + const snapshot = api.createSnapshot({ openFiles: ["/src/index.ts"] }); const defaultProject = snapshot.getDefaultProjectForFile("/src/index.ts"); assert.ok(defaultProject, "should find a project for the opened file"); assert.equal( @@ -6548,12 +6573,12 @@ describe("getDefaultProjectForFile", () => { "/src/index.ts": `export const x = 1;`, }); - const opened = api.updateSnapshot({ openProjects: ["/tsconfig.json"] }); - assert.ok(opened.getProject("/tsconfig.json"), "project should be open after openProjects"); + const opened = api.createSnapshot({ openProjects: ["/tsconfig.json"] }); + assert.ok(opened.getConfiguredProject("/tsconfig.json"), "project should be open after openProjects"); - const closed = api.updateSnapshot({ closeProjects: ["/tsconfig.json"] }); + const closed = api.createSnapshot({ closeProjects: ["/tsconfig.json"] }); assert.equal( - closed.getProject("/tsconfig.json"), + closed.getConfiguredProject("/tsconfig.json"), undefined, "project should be unloaded after closeProjects", ); @@ -6567,14 +6592,14 @@ describe("getDefaultProjectForFile", () => { "/node_modules/my-lib/index.d.ts": `export declare const foo: string;`, }); - api.updateSnapshot({ openProject: "/tsconfig.json" }); - const opened = api.updateSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); + api.createSnapshot({ openProject: "/tsconfig.json" }); + const opened = api.createSnapshot({ openFiles: ["/node_modules/my-lib/index.d.ts"] }); assert.ok( opened.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"), "file should resolve to a project after openFiles", ); - const closed = api.updateSnapshot({ closeFiles: ["/node_modules/my-lib/index.d.ts"] }); + const closed = api.createSnapshot({ closeFiles: ["/node_modules/my-lib/index.d.ts"] }); assert.equal( closed.getDefaultProjectForFile("/node_modules/my-lib/index.d.ts"), undefined, @@ -6603,8 +6628,8 @@ describe("Program - emit", () => { fs: fs, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = project.program.emit(); assert.deepEqual(result, { diagnostics: [], @@ -6635,8 +6660,8 @@ describe("Program - emit", () => { fs: fs, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = project.program.emit(EmitOnly.OnlyDts); assert.deepEqual(result, { diagnostics: [], @@ -6665,8 +6690,8 @@ describe("Program - emit", () => { fs: fs, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = project.program.emit(EmitOnly.OnlyJs); assert.deepEqual(result, { diagnostics: [], @@ -6691,8 +6716,8 @@ describe("Program - emit", () => { const { api: disposableAPI, fs } = spawnAPIWithFS({ ...files }); using api = disposableAPI; - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = project.program.emitToString(EmitOnly.OnlyDts); assert.deepEqual([...result.outputFiles.keys()], [ "/dist/src/index.d.ts", @@ -6715,8 +6740,8 @@ describe("Program - emit", () => { }); using api = disposableAPI; - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = project.program.emit(); assert.deepEqual( @@ -6757,8 +6782,8 @@ describe("Program - emit", () => { }); using api = disposableAPI; - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const result = project.program.emit(); assert.equal(result.emitSkipped, true); assert.ok(result.diagnostics.some(d => d.code === 1109)); @@ -6779,8 +6804,8 @@ describe("Program - emit", () => { }); using api = disposableAPI; - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; assert.deepEqual(project.program.emit(), { diagnostics: [], emitSkipped: false, @@ -6797,8 +6822,8 @@ describe("Program - emit", () => { test("emit rejects unknown files and invalid emitOnly values", () => { using api = spawnAPI({ ...files }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; let error: unknown; try { @@ -6839,8 +6864,8 @@ describe("Timing", () => { assert.equal(info.recentRequests.length, 0); // Exercise a JSON request and a binary source-file request. - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -6898,8 +6923,8 @@ describe("Timing", () => { collectTiming: true, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sourceFile = project.program.getSourceFile("/src/index.ts"); assert.ok(sourceFile); @@ -6967,21 +6992,21 @@ describe("runWithTemporaryFileUpdate", () => { "/src/index.ts": `export const x: number = 1;`, }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; // The original content type-checks cleanly. const baseDiags = project.program.getSemanticDiagnostics("/src/index.ts"); assert.equal(baseDiags.length, 0); // Keep a newer snapshot active to verify any active snapshot can be the base. - const latestSnapshot = api.updateSnapshot(); + const latestSnapshot = api.createSnapshot(); assert.notEqual(latestSnapshot.id, snapshot.id); // Inside the callback, the file has the temporary (erroneous) content. let errorCount = -1; api.runWithTemporaryFileUpdate(snapshot, "/src/index.ts", `export const x: string = 1;`, tempSnapshot => { - const tempProject = tempSnapshot.getProject("/tsconfig.json")!; + const tempProject = tempSnapshot.getConfiguredProject("/tsconfig.json")!; const diags = tempProject.program.getSemanticDiagnostics("/src/index.ts"); errorCount = diags.length; }); @@ -6991,12 +7016,35 @@ describe("runWithTemporaryFileUpdate", () => { const afterDiags = project.program.getSemanticDiagnostics("/src/index.ts"); assert.equal(afterDiags.length, 0); - // Subsequent regular updates still work and diff against the real latest snapshot. - const snapshot2 = api.updateSnapshot(); - const project2 = snapshot2.getProject("/tsconfig.json")!; + // A subsequent independent snapshot can request the project again. + const snapshot2 = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project2 = snapshot2.getConfiguredProject("/tsconfig.json")!; const diags2 = project2.program.getSemanticDiagnostics("/src/index.ts"); assert.equal(diags2.length, 0); }); + + test("reconstructs projects omitted from the response diff", () => { + using api = spawnAPI({ + "/first/tsconfig.json": `{}`, + "/first/index.ts": `export const first = 1;`, + "/second/tsconfig.json": `{}`, + "/second/index.ts": `export const second = 1;`, + }); + + const snapshot = api.createSnapshot({ + openProjects: ["/first/tsconfig.json", "/second/tsconfig.json"], + }); + const originalSecondProject = snapshot.getConfiguredProject("/second/tsconfig.json")!; + + api.runWithTemporaryFileUpdate(snapshot, "/first/index.ts", `export const first = 2;`, tempSnapshot => { + assert.equal(tempSnapshot.getProjects().length, 2); + assert.ok(tempSnapshot.getConfiguredProject("/first/tsconfig.json")); + const secondProject = tempSnapshot.getConfiguredProject("/second/tsconfig.json"); + assert.ok(secondProject); + assert.notStrictEqual(secondProject, originalSecondProject); + assert.equal((secondProject.program.getSourceFileNames()).includes("/second/index.ts"), true); + }); + }); }); function spawnAPIWithFS(files: Record = { ...defaultFiles }): { api: API; fs: FileSystem; } { diff --git a/packages/typescript/test/sync/ast.test.ts b/packages/typescript/test/sync/ast.test.ts index 1783da4ac3185..59c8a6fb4604c 100644 --- a/packages/typescript/test/sync/ast.test.ts +++ b/packages/typescript/test/sync/ast.test.ts @@ -576,8 +576,8 @@ function spawnAPI(files: Record = { } function getRemoteSourceFile(api: API, configPath: string, filePath: string) { - const snapshot = api.updateSnapshot({ openProject: configPath }); - const project = snapshot.getProject(configPath)!; + const snapshot = api.createSnapshot({ openProject: configPath }); + const project = snapshot.getConfiguredProject(configPath)!; return project.program.getSourceFile(filePath)!; } diff --git a/packages/typescript/test/sync/astnav.test.ts b/packages/typescript/test/sync/astnav.test.ts index 2e82fb944eca8..3747e638d013b 100644 --- a/packages/typescript/test/sync/astnav.test.ts +++ b/packages/typescript/test/sync/astnav.test.ts @@ -108,8 +108,8 @@ describe("astnav", () => { }), }); - const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); - const project = snapshot.getProject("/tsconfig.json")!; + const snapshot = api.createSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getConfiguredProject("/tsconfig.json")!; const sf = project.program.getSourceFile("/src/testFile.ts"); assert.ok(sf, "Failed to get source file from API"); sourceFile = sf; diff --git a/tools/gen-proto/main.go b/tools/gen-proto/main.go index ce65bc39772fb..5461b5a21e34a 100644 --- a/tools/gen-proto/main.go +++ b/tools/gen-proto/main.go @@ -350,6 +350,7 @@ type typeRenderer struct { seen map[*types.TypeName]bool names map[string]*types.TypeName imports map[string][]string + typeImports map[string][]string docs map[types.Object]string packages map[string]*packages.Package documentIdentifier *types.TypeName @@ -361,6 +362,7 @@ func newTypeRenderer(apiPackage *packages.Package) *typeRenderer { seen: make(map[*types.TypeName]bool), names: make(map[string]*types.TypeName), imports: make(map[string][]string), + typeImports: make(map[string][]string), docs: make(map[types.Object]string), packages: make(map[string]*packages.Package), } @@ -492,6 +494,13 @@ func (r *typeRenderer) namedType(named *types.Named) string { case r.apiPackagePath + ".DocumentIdentifier": r.documentIdentifier = obj return "DocumentIdentifier" + case r.apiPackagePath + ".EnsurePrograms": + return "EnsurePrograms" + case r.apiPackagePath + ".ProjectID": + r.importTypeOnly("Path", "../ast/index.ts") + return "ProjectId" + case r.apiPackagePath + ".SyntheticProjectID": + return "SyntheticProjectId" case "github.com/microsoft/TypeScript/tsc/internal/packagejson.JSONValue": return "unknown" case "github.com/microsoft/TypeScript/tsc/internal/json.Value": @@ -597,14 +606,32 @@ func (r *typeRenderer) declarations() (string, error) { writeDoc(&out, "", r.docs[r.documentIdentifier]) out.WriteString("export type DocumentIdentifier = string | { uri: string; };\n\n") } + out.WriteString("export type EnsurePrograms = true | readonly ProjectId[];\n\n") + out.WriteString("export type InferredProjectId = string & { __inferredProjectIdBrand: any; };\n") + out.WriteString("export type ConfiguredProjectId = Path & { __configuredProjectIdBrand: any; };\n") + out.WriteString("export type SyntheticProjectId = string & { __syntheticProjectIdBrand: any; };\n") + out.WriteString("export type ProjectId = InferredProjectId | ConfiguredProjectId | SyntheticProjectId;\n\n") for len(r.queued) > 0 { named := r.queued[0] r.queued = r.queued[1:] structType := named.Underlying().(*types.Struct) isParams := strings.HasSuffix(named.Obj().Name(), "Params") writeDoc(&out, "", r.docs[named.Obj()]) - fmt.Fprintf(&out, "export interface %s {\n", exportedName(named.Obj().Name())) + var embedded []string + for field := range structType.Fields() { + if field.Embedded() { + embedded = append(embedded, r.typeString(field.Type(), false)) + } + } + fmt.Fprintf(&out, "export interface %s", exportedName(named.Obj().Name())) + if len(embedded) > 0 { + fmt.Fprintf(&out, " extends %s", strings.Join(embedded, ", ")) + } + out.WriteString(" {\n") for i := range structType.NumFields() { + if structType.Field(i).Embedded() { + continue + } field, include, optional, nonnil, deprecated, internal := jsonField(structType, i) if !include || deprecated || internal { continue @@ -647,6 +674,16 @@ func jsDocLine(line string) string { func (r *typeRenderer) importDeclarations() string { var out bytes.Buffer + typePaths := make([]string, 0, len(r.typeImports)) + for path := range r.typeImports { + typePaths = append(typePaths, path) + } + sort.Strings(typePaths) + for _, path := range typePaths { + names := r.typeImports[path] + sort.Strings(names) + fmt.Fprintf(&out, "import type { %s } from %q;\n", strings.Join(names, ", "), path) + } paths := make([]string, 0, len(r.imports)) for path := range r.imports { paths = append(paths, path) @@ -664,6 +701,13 @@ func (r *typeRenderer) importDeclarations() string { return out.String() } +func (r *typeRenderer) importTypeOnly(name string, path string) string { + if !slices.Contains(r.typeImports[path], name) { + r.typeImports[path] = append(r.typeImports[path], name) + } + return name +} + func (r *typeRenderer) importType(name string, path string) string { if !slices.Contains(r.imports[path], name) { r.imports[path] = append(r.imports[path], name) diff --git a/tools/gen-proto/main_test.go b/tools/gen-proto/main_test.go index 12e4876ace63f..cd6b34153866a 100644 --- a/tools/gen-proto/main_test.go +++ b/tools/gen-proto/main_test.go @@ -26,12 +26,20 @@ func TestGenerate(t *testing.T) { for _, expected := range []string{ `release: APIMethod;`, - `updateSnapshot: APIMethod;`, + `updateSnapshot: APIMethod;`, `initialize: APIMethod;`, `export type DocumentIdentifier = string | { uri: string; };`, `export interface ReleaseParams`, `export interface UpdateSnapshotParams`, + `export interface CreateSnapshotParams extends SnapshotRequestChangesParams`, + `export interface LanguageServerSnapshotChanges extends SnapshotRequestChangesParams`, `openProjects?: readonly DocumentIdentifier[];`, + `export type EnsurePrograms = true | readonly ProjectId[];`, + `export type InferredProjectId = string & { __inferredProjectIdBrand: any; };`, + `export type ConfiguredProjectId = Path & { __configuredProjectIdBrand: any; };`, + `export type SyntheticProjectId = string & { __syntheticProjectIdBrand: any; };`, + `export type ProjectId = InferredProjectId | ConfiguredProjectId | SyntheticProjectId;`, + `ensurePrograms?: EnsurePrograms;`, `snapshot: number;`, `file: DocumentIdentifier;`, `jsx?: JsxEmit;`, @@ -63,6 +71,10 @@ export interface CompilerOptions`, data: string; }`, `projects: ProjectResponse[];`, + `operation: SnapshotOperationResponse;`, + `createdPrograms?: SyntheticProjectId[];`, + `openedFiles?: OpenedFileOperationResult[];`, + `dirty: boolean;`, `entries: CompletionEntryResponse[];`, `outputFiles: EmitOutputFile[];`, `/** Path is a normalized path on disk. */ diff --git a/tsc/internal/api/proto.go b/tsc/internal/api/proto.go index 107fbc580057e..a2cc87e45bd50 100644 --- a/tsc/internal/api/proto.go +++ b/tsc/internal/api/proto.go @@ -33,18 +33,23 @@ var ( type Method string type ( - SnapshotID uint64 - ProjectID string - SymbolID uint64 - TypeID uint32 - SignatureID uint64 - NodeHandle string + SnapshotID uint64 + ProjectID string + SyntheticProjectID string + SymbolID uint64 + TypeID uint32 + SignatureID uint64 + NodeHandle string ) func ProjectHandle(p *project.Project) ProjectID { return ProjectID(p.ID()) } +func SyntheticProjectHandle(p *project.Project) SyntheticProjectID { + return SyntheticProjectID(p.ID()) +} + func SymbolHandle(symbol *ast.Symbol) SymbolID { return SymbolID(ast.GetSymbolId(symbol)) } @@ -66,42 +71,43 @@ const ( MethodBatchRequests Method = "batchRequests" - MethodInitialize Method = "initialize" - MethodUpdateSnapshot Method = "updateSnapshot" - MethodUpdateTemporarySnapshot Method = "updateTemporarySnapshot" - MethodCreateProgram Method = "createProgram" - MethodParseCommandLine Method = "parseCommandLine" - MethodReadConfigFile Method = "readConfigFile" - MethodParseJsonConfigFile Method = "parseJsonConfigFileContent" - MethodParseConfigFile Method = "parseConfigFile" - MethodTranspileModule Method = "transpileModule" - MethodTranspileModuleFromFile Method = "transpileModuleFromFile" - MethodTranspileDeclaration Method = "transpileDeclaration" - MethodTranspileDeclarationFromFile Method = "transpileDeclarationFromFile" - MethodGetDefaultProjectForFile Method = "getDefaultProjectForFile" - MethodGetSymbolAtPosition Method = "getSymbolAtPosition" - MethodGetSymbolsAtPositions Method = "getSymbolsAtPositions" - MethodGetSymbolAtLocation Method = "getSymbolAtLocation" - MethodGetSymbolsAtLocations Method = "getSymbolsAtLocations" - MethodGetSymbolOfSourceFile Method = "getSymbolOfSourceFile" - MethodGetSymbolsOfSourceFiles Method = "getSymbolsOfSourceFiles" - MethodGetTypeOfSymbol Method = "getTypeOfSymbol" - MethodGetTypesOfSymbols Method = "getTypesOfSymbols" - MethodGetDeclaredTypeOfSymbol Method = "getDeclaredTypeOfSymbol" - MethodGetNonMissingTypeOfSymbol Method = "getNonMissingTypeOfSymbol" - MethodGetSourceFile Method = "getSourceFile" - MethodGetSourceFileNames Method = "getSourceFileNames" - MethodGetSourceFileMetadata Method = "getSourceFileMetadata" - MethodGetConfigFileNames Method = "getConfigFileNames" - MethodGetConfigSourceFile Method = "getConfigSourceFile" - MethodResolveName Method = "resolveName" - MethodGetSymbolsInScope Method = "getSymbolsInScope" - MethodGetSignaturesOfType Method = "getSignaturesOfType" - MethodGetResolvedSignature Method = "getResolvedSignature" - MethodGetTypeAtLocation Method = "getTypeAtLocation" - MethodGetTypeAtLocations Method = "getTypeAtLocations" - MethodGetTypeAtPosition Method = "getTypeAtPosition" - MethodGetTypesAtPositions Method = "getTypesAtPositions" + MethodInitialize Method = "initialize" + MethodCreateSnapshot Method = "createSnapshot" + MethodUpdateSnapshot Method = "updateSnapshot" + MethodGetCurrentLanguageServerSnapshot Method = "getCurrentLanguageServerSnapshot" + MethodUpdateTemporarySnapshot Method = "updateTemporarySnapshot" + MethodParseCommandLine Method = "parseCommandLine" + MethodReadConfigFile Method = "readConfigFile" + MethodParseJsonConfigFile Method = "parseJsonConfigFileContent" + MethodParseConfigFile Method = "parseConfigFile" + MethodTranspileModule Method = "transpileModule" + MethodTranspileModuleFromFile Method = "transpileModuleFromFile" + MethodTranspileDeclaration Method = "transpileDeclaration" + MethodTranspileDeclarationFromFile Method = "transpileDeclarationFromFile" + MethodGetDefaultProjectForFile Method = "getDefaultProjectForFile" + MethodGetSymbolAtPosition Method = "getSymbolAtPosition" + MethodGetSymbolsAtPositions Method = "getSymbolsAtPositions" + MethodGetSymbolAtLocation Method = "getSymbolAtLocation" + MethodGetSymbolsAtLocations Method = "getSymbolsAtLocations" + MethodGetSymbolOfSourceFile Method = "getSymbolOfSourceFile" + MethodGetSymbolsOfSourceFiles Method = "getSymbolsOfSourceFiles" + MethodGetTypeOfSymbol Method = "getTypeOfSymbol" + MethodGetTypesOfSymbols Method = "getTypesOfSymbols" + MethodGetDeclaredTypeOfSymbol Method = "getDeclaredTypeOfSymbol" + MethodGetNonMissingTypeOfSymbol Method = "getNonMissingTypeOfSymbol" + MethodGetSourceFile Method = "getSourceFile" + MethodGetSourceFileNames Method = "getSourceFileNames" + MethodGetSourceFileMetadata Method = "getSourceFileMetadata" + MethodGetConfigFileNames Method = "getConfigFileNames" + MethodGetConfigSourceFile Method = "getConfigSourceFile" + MethodResolveName Method = "resolveName" + MethodGetSymbolsInScope Method = "getSymbolsInScope" + MethodGetSignaturesOfType Method = "getSignaturesOfType" + MethodGetResolvedSignature Method = "getResolvedSignature" + MethodGetTypeAtLocation Method = "getTypeAtLocation" + MethodGetTypeAtLocations Method = "getTypeAtLocations" + MethodGetTypeAtPosition Method = "getTypeAtPosition" + MethodGetTypesAtPositions Method = "getTypesAtPositions" // Symbol sub-property methods MethodGetParentOfSymbol Method = "getParentOfSymbol" @@ -342,34 +348,84 @@ type APIFileChanges struct { Deleted []DocumentIdentifier `json:"deleted,omitempty"` } -// UpdateSnapshotParams are the parameters for creating a new snapshot. -// All fields are optional. With no fields set, the server adopts the latest LSP state. -type UpdateSnapshotParams struct { - // Snapshot, when set, requires this to be the latest active snapshot and layers - // FileSystem over that snapshot's filesystem. Used by Snapshot.update. - Snapshot SnapshotID `json:"snapshot,omitempty"` +// SnapshotRequestChangesParams describes project, file, and program changes to apply +// while creating or updating a snapshot. +type SnapshotRequestChangesParams struct { // OpenProjects lists tsconfig.json files to open/load in the new snapshot. - // Opens are ref-counted and persist across snapshots until closed. OpenProjects []DocumentIdentifier `json:"openProjects,omitempty"` // CloseProjects lists tsconfig.json files to release in the new snapshot. // A project is only unloaded once every API client that opened it closes it. CloseProjects []DocumentIdentifier `json:"closeProjects,omitempty"` - // FileChanges describes file system changes since the last snapshot. - FileChanges *APIFileChanges `json:"fileChanges,omitempty"` - // FileSystem supplies file contents and directory listings for the new snapshot. - // A full filesystem is canonical and total. A filesystem layer is checked - // before falling back to the host filesystem. - FileSystem *requestfilesystem.RequestFileSystem `json:"fileSystem,omitempty"` - // OpenFiles lists files to keep open for the API client, mirroring LSP's + // OpenFiles lists files to open in the new snapshot, mirroring LSP's // textDocument/didOpen. For each file, ancestor directories are searched for a // tsconfig that contains it; if found, that configured project is loaded and // becomes the file's default project. Otherwise the file is loaded into the // inferred project (e.g. a node_modules d.ts not in any project's import graph). - // Opens persist across snapshots until the file is closed. OpenFiles []DocumentIdentifier `json:"openFiles,omitempty"` // CloseFiles lists files to release in the new snapshot. A file is only fully // closed once every API client that opened it closes it. CloseFiles []DocumentIdentifier `json:"closeFiles,omitempty"` + // CreatePrograms describes synthetic programs to create in the snapshot. + CreatePrograms []*CreateSnapshotProgramParams `json:"createPrograms,omitempty"` + // RemovePrograms lists synthetic project handles to remove from the snapshot. + RemovePrograms []SyntheticProjectID `json:"removePrograms,omitempty"` + // EnsurePrograms identifies projects whose programs should be updated if dirty, + // or all contained projects when true. + EnsurePrograms *EnsurePrograms `json:"ensurePrograms,omitempty"` +} + +type EnsurePrograms struct { + All bool + Projects []ProjectID +} + +var _ json.UnmarshalerFrom = (*EnsurePrograms)(nil) + +func (e *EnsurePrograms) UnmarshalJSONFrom(dec *json.Decoder) error { + value, err := dec.ReadValue() + if err != nil { + return err + } + if string(value) == "true" { + e.All = true + return nil + } + if value.Kind() != '[' { + return errors.New("ensurePrograms must be true or an array of project IDs") + } + return json.Unmarshal(value, &e.Projects) +} + +// CreateSnapshotParams are the parameters for creating a new independent snapshot. +type CreateSnapshotParams struct { + SnapshotRequestChangesParams + // FileChanges describes host file system changes to invalidate while creating the snapshot. + FileChanges *APIFileChanges `json:"fileChanges,omitempty"` + // FileSystem supplies file contents and directory listings for the new snapshot. + // A full filesystem is canonical and total. A filesystem layer is checked + // before falling back to the base snapshot or host filesystem. + FileSystem *requestfilesystem.RequestFileSystem `json:"fileSystem,omitempty"` +} + +type CreateSnapshotProgramParams struct { + RootFiles []DocumentIdentifier `json:"rootFiles"` + Options CreateProgramOptions `json:"options"` +} + +type UpdateSnapshotParams struct { + Snapshot SnapshotID `json:"snapshot"` + Changes *CreateSnapshotParams `json:"changes,omitempty"` +} + +type GetCurrentLanguageServerSnapshotParams struct { + BaseSnapshot SnapshotID `json:"baseSnapshot,omitempty"` + Changes *LanguageServerSnapshotChanges `json:"changes,omitempty"` +} + +// LanguageServerSnapshotChanges describes API-driven changes to adopt into the +// language server's canonical state. +type LanguageServerSnapshotChanges struct { + SnapshotRequestChangesParams } // UpdateTemporarySnapshotParams are the parameters for creating a temporary @@ -383,29 +439,12 @@ type UpdateTemporarySnapshotParams struct { NewText string `json:"newText"` } -type CreateProgramParams struct { - RootFiles []DocumentIdentifier `json:"rootFiles"` - CreateProgramOptions CreateProgramOptions `json:"createProgramOptions"` - OldProgram *CreateProgramOldProgramParams `json:"oldProgram,omitempty"` - FileChanges *APIFileChanges `json:"fileChanges,omitempty"` -} - type CreateProgramOptions struct { CompilerOptions core.CompilerOptions `json:"compilerOptions"` ProjectReferences []*core.ProjectReference `json:"projectReferences,omitempty"` ConfigFileParsingDiagnostics []*DiagnosticResponse `json:"configFileParsingDiagnostics,omitempty"` } -type CreateProgramOldProgramParams struct { - Snapshot SnapshotID `json:"snapshot,omitempty"` - Project ProjectID `json:"project,omitempty"` -} - -type CreateProgramResponse struct { - Snapshot SnapshotID `json:"snapshot"` - Project *ProjectResponse `json:"project"` -} - // ProjectFileChanges describes what source files changed within a single project. type ProjectFileChanges struct { // ChangedFiles lists source file paths whose content differs. @@ -414,8 +453,8 @@ type ProjectFileChanges struct { DeletedFiles []tspath.Path `json:"deletedFiles,omitempty"` } -// SnapshotChanges describes what changed between the previous latest snapshot -// and the newly created snapshot. Changes are reported per-project so clients +// SnapshotChanges describes what changed between a response base and a new +// snapshot. Changes are reported per-project so clients // can track cache refs at the (snapshot, project) level. type SnapshotChanges struct { // ChangedProjects maps project handles to the file changes within that project. @@ -426,56 +465,68 @@ type SnapshotChanges struct { RemovedProjects []ProjectID `json:"removedProjects,omitempty"` } -// UpdateSnapshotResponse is returned by updateSnapshot. -type UpdateSnapshotResponse struct { +// CreateSnapshotResponse is returned by createSnapshot. +type CreateSnapshotResponse struct { // Snapshot is the handle for the newly created snapshot. Snapshot SnapshotID `json:"snapshot"` - // Projects is the list of projects in the snapshot. + // Projects contains all projects when no response base was supplied, or only + // projects added or replaced relative to that base. Projects []*ProjectResponse `json:"projects" nonnil:"true"` - // Changes describes source file differences from the previous snapshot. - // Nil for the first snapshot in a session. + // Changes describes source file differences from the response base. Changes *SnapshotChanges `json:"changes,omitempty"` + // Operation describes results correlated with the request that produced the snapshot. + Operation *SnapshotOperationResponse `json:"operation" nonnil:"true"` +} + +type SnapshotOperationResponse struct { + CreatedPrograms *[]SyntheticProjectID `json:"createdPrograms,omitzero"` + OpenedFiles *[]*OpenedFileOperationResult `json:"openedFiles,omitzero"` +} + +type OpenedFileOperationResult struct { + Project ProjectID `json:"project"` } var unmarshalers = map[Method]func([]byte) (any, error){ - MethodBatchRequests: unmarshallerFor[BatchRequestsParams], - MethodRelease: unmarshallerFor[ReleaseParams], - MethodInitialize: noParams, - MethodUpdateSnapshot: unmarshallerFor[UpdateSnapshotParams], - MethodUpdateTemporarySnapshot: unmarshallerFor[UpdateTemporarySnapshotParams], - MethodCreateProgram: unmarshallerFor[CreateProgramParams], - MethodParseCommandLine: unmarshallerFor[ParseCommandLineParams], - MethodReadConfigFile: unmarshallerFor[ReadConfigFileParams], - MethodParseJsonConfigFile: unmarshallerFor[ParseJsonConfigFileContentParams], - MethodParseConfigFile: unmarshallerFor[ParseConfigFileParams], - MethodTranspileModule: unmarshallerFor[TranspileParams], - MethodTranspileModuleFromFile: unmarshallerFor[TranspileFromFileParams], - MethodTranspileDeclaration: unmarshallerFor[TranspileParams], - MethodTranspileDeclarationFromFile: unmarshallerFor[TranspileFromFileParams], - MethodGetDefaultProjectForFile: unmarshallerFor[GetDefaultProjectForFileParams], - MethodGetSourceFile: unmarshallerFor[GetSourceFileParams], - MethodGetSourceFileNames: unmarshallerFor[GetSourceFileNamesParams], - MethodGetSourceFileMetadata: unmarshallerFor[GetSourceFileParams], - MethodGetConfigFileNames: unmarshallerFor[GetProjectDiagnosticsParams], - MethodGetConfigSourceFile: unmarshallerFor[GetSourceFileParams], - MethodGetSymbolAtPosition: unmarshallerFor[GetSymbolAtPositionParams], - MethodGetSymbolsAtPositions: unmarshallerFor[GetSymbolsAtPositionsParams], - MethodGetSymbolAtLocation: unmarshallerFor[GetSymbolAtLocationParams], - MethodGetSymbolsAtLocations: unmarshallerFor[GetSymbolsAtLocationsParams], - MethodGetSymbolOfSourceFile: unmarshallerFor[GetSymbolOfSourceFileParams], - MethodGetSymbolsOfSourceFiles: unmarshallerFor[GetSymbolsOfSourceFilesParams], - MethodGetTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], - MethodGetTypesOfSymbols: unmarshallerFor[GetTypesOfSymbolsParams], - MethodGetDeclaredTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], - MethodGetNonMissingTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], - MethodResolveName: unmarshallerFor[ResolveNameParams], - MethodGetSymbolsInScope: unmarshallerFor[GetSymbolsInScopeParams], - MethodGetSignaturesOfType: unmarshallerFor[GetSignaturesOfTypeParams], - MethodGetResolvedSignature: unmarshallerFor[GetResolvedSignatureParams], - MethodGetTypeAtLocation: unmarshallerFor[GetTypeAtLocationParams], - MethodGetTypeAtLocations: unmarshallerFor[GetTypeAtLocationsParams], - MethodGetTypeAtPosition: unmarshallerFor[GetTypeAtPositionParams], - MethodGetTypesAtPositions: unmarshallerFor[GetTypesAtPositionsParams], + MethodBatchRequests: unmarshallerFor[BatchRequestsParams], + MethodRelease: unmarshallerFor[ReleaseParams], + MethodInitialize: noParams, + MethodCreateSnapshot: unmarshallerFor[CreateSnapshotParams], + MethodUpdateSnapshot: unmarshallerFor[UpdateSnapshotParams], + MethodGetCurrentLanguageServerSnapshot: unmarshallerFor[GetCurrentLanguageServerSnapshotParams], + MethodUpdateTemporarySnapshot: unmarshallerFor[UpdateTemporarySnapshotParams], + MethodParseCommandLine: unmarshallerFor[ParseCommandLineParams], + MethodReadConfigFile: unmarshallerFor[ReadConfigFileParams], + MethodParseJsonConfigFile: unmarshallerFor[ParseJsonConfigFileContentParams], + MethodParseConfigFile: unmarshallerFor[ParseConfigFileParams], + MethodTranspileModule: unmarshallerFor[TranspileParams], + MethodTranspileModuleFromFile: unmarshallerFor[TranspileFromFileParams], + MethodTranspileDeclaration: unmarshallerFor[TranspileParams], + MethodTranspileDeclarationFromFile: unmarshallerFor[TranspileFromFileParams], + MethodGetDefaultProjectForFile: unmarshallerFor[GetDefaultProjectForFileParams], + MethodGetSourceFile: unmarshallerFor[GetSourceFileParams], + MethodGetSourceFileNames: unmarshallerFor[GetSourceFileNamesParams], + MethodGetSourceFileMetadata: unmarshallerFor[GetSourceFileParams], + MethodGetConfigFileNames: unmarshallerFor[GetProjectDiagnosticsParams], + MethodGetConfigSourceFile: unmarshallerFor[GetSourceFileParams], + MethodGetSymbolAtPosition: unmarshallerFor[GetSymbolAtPositionParams], + MethodGetSymbolsAtPositions: unmarshallerFor[GetSymbolsAtPositionsParams], + MethodGetSymbolAtLocation: unmarshallerFor[GetSymbolAtLocationParams], + MethodGetSymbolsAtLocations: unmarshallerFor[GetSymbolsAtLocationsParams], + MethodGetSymbolOfSourceFile: unmarshallerFor[GetSymbolOfSourceFileParams], + MethodGetSymbolsOfSourceFiles: unmarshallerFor[GetSymbolsOfSourceFilesParams], + MethodGetTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], + MethodGetTypesOfSymbols: unmarshallerFor[GetTypesOfSymbolsParams], + MethodGetDeclaredTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], + MethodGetNonMissingTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], + MethodResolveName: unmarshallerFor[ResolveNameParams], + MethodGetSymbolsInScope: unmarshallerFor[GetSymbolsInScopeParams], + MethodGetSignaturesOfType: unmarshallerFor[GetSignaturesOfTypeParams], + MethodGetResolvedSignature: unmarshallerFor[GetResolvedSignatureParams], + MethodGetTypeAtLocation: unmarshallerFor[GetTypeAtLocationParams], + MethodGetTypeAtLocations: unmarshallerFor[GetTypeAtLocationsParams], + MethodGetTypeAtPosition: unmarshallerFor[GetTypeAtPositionParams], + MethodGetTypesAtPositions: unmarshallerFor[GetTypesAtPositionsParams], MethodGetParentOfSymbol: unmarshallerFor[GetSymbolPropertyParams], MethodGetMembersOfSymbol: unmarshallerFor[GetSymbolPropertyParams], @@ -750,6 +801,7 @@ type ProjectResponse struct { Id ProjectID `json:"id"` ConfigFileName string `json:"configFileName"` CurrentDirectory string `json:"currentDirectory"` + Dirty bool `json:"dirty"` ParsedCommandLine *ConfigFileResponse `json:"parsedCommandLine" nonnil:"true"` // Deprecated: Use parsedCommandLine.fileNames. RootFiles []string `json:"rootFiles" nonnil:"true"` @@ -818,6 +870,7 @@ func NewProjectResponse(p *project.Project) *ProjectResponse { Id: ProjectHandle(p), ConfigFileName: p.Name(), CurrentDirectory: p.CurrentDirectory(), + Dirty: p.IsDirty(), ParsedCommandLine: NewConfigFileResponse(p.CommandLine), RootFiles: p.CommandLine.FileNames(), CompilerOptions: p.CommandLine.CompilerOptions(), diff --git a/tsc/internal/api/proto_test.go b/tsc/internal/api/proto_test.go index e5add0c137345..dec337b1029ed 100644 --- a/tsc/internal/api/proto_test.go +++ b/tsc/internal/api/proto_test.go @@ -64,6 +64,21 @@ func TestDocumentIdentifierUnmarshalJSON(t *testing.T) { } } +func TestEnsureProgramsUnmarshalJSON(t *testing.T) { + t.Parallel() + + var all api.EnsurePrograms + assert.NilError(t, json.Unmarshal([]byte(`true`), &all)) + assert.Equal(t, all.All, true) + + var projects api.EnsurePrograms + assert.NilError(t, json.Unmarshal([]byte(`["/tsconfig.json","/dev/null/synthetic/1"]`), &projects)) + assert.DeepEqual(t, projects.Projects, []api.ProjectID{"/tsconfig.json", "/dev/null/synthetic/1"}) + + var invalid api.EnsurePrograms + assert.ErrorContains(t, json.Unmarshal([]byte(`false`), &invalid), "must be true or an array") +} + func TestNewDiagnosticResponseIncludesFormattingContext(t *testing.T) { t.Parallel() diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index de5b7c7d5ddd6..8275b9aa9a015 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -49,6 +49,9 @@ type snapshotData struct { fileSystem vfs.FS refCount int + openProjects collections.Set[tspath.Path] + openFiles collections.Set[tspath.Path] + // Symbol IDs come from ast.GetSymbolId, a global atomic counter, so the same // *ast.Symbol pointer always has the same unique ID across all projects in the // snapshot. Symbols are registered snapshot-wide to ensure identity semantics: @@ -97,7 +100,7 @@ func (sd *snapshotData) getProgram(projectHandle ProjectID) (*compiler.Program, // getProject looks up a project from a project handle within this snapshot. func (sd *snapshotData) getProject(projectHandle ProjectID) (*project.Project, error) { - projectName := parseProjectHandle(projectHandle) + projectName := tspath.Path(projectHandle) proj := sd.snapshot.ProjectCollection.GetProjectByPath(projectName) if proj == nil { return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName) @@ -382,14 +385,11 @@ func (sd *snapshotData) registerSignature(projectID ProjectID, sig *checker.Sign // The session supports multiple active snapshots, each with their own // symbol and type registries for maintaining object identity. type Session struct { - id string - snapshotHost *project.SnapshotHost - withLocale func(context.Context) context.Context - projectSession *project.Session - // compatibilitySnapshot is the standalone API session's canonical snapshot. - // It preserves the legacy linear updateSnapshot behavior. - compatibilitySnapshot *project.Snapshot - compatibilityMu sync.Mutex + id string + snapshotHost *project.SnapshotHost + ownsSnapshotHost bool + withLocale func(context.Context) context.Context + projectSession *project.Session closeOnce sync.Once @@ -401,37 +401,23 @@ type Session struct { // snapshots maps snapshot handles to their data. Each snapshot has its own // symbol/type registries. // - // snapshotsMu guards the snapshots map and latestSnapshot. It is held only for + // snapshotsMu guards the snapshots map. It is held only for // short, map-bounded critical sections, never across slow work like a project // snapshot update or checker queries. Read handlers (getSnapshotData and the // language-service handlers built on it) take it for reading; handleRelease and - // the bookkeeping tail of handleUpdateSnapshot take it for writing. This is what + // snapshot creation bookkeeping takes it for writing. This is what // lets queries against an existing snapshot run concurrently with the building of // the next one. snapshots map[SnapshotID]*snapshotData snapshotsMu sync.RWMutex - // latestSnapshot tracks the most recently created snapshot, used as the diff base - // for the next update. Guarded by snapshotsMu. - latestSnapshot SnapshotID + // openProjects, openFiles, and createdPrograms are the canonical LSP-state resources + // owned by this API client. Guarded by languageServerUpdateMu. + openProjects collections.Set[tspath.Path] + openFiles collections.Set[tspath.Path] + createdPrograms collections.Set[int] - // openProjects and openFiles track the projects and files this session - // currently holds open in the API snapshot state. The session holds at most - // one ref per project/file (opens are idempotent), so it can release exactly - // those refs on Close and never send a close for a ref it doesn't hold. - // Guarded by updateMu. - openProjects collections.Set[tspath.Path] - openFiles collections.Set[tspath.Path] - - // updateMu serializes the whole of handleUpdateSnapshot (and releaseOpenRefs) - // against other updates. Unlike snapshotsMu it is held across the slow - // projectSession.APIUpdate call, because building the request from - // openProjects/openFiles, applying it, committing the ref tracking, and advancing - // latestSnapshot must be one atomic step; otherwise concurrent updates could - // double-count refs or diff against a non-adjacent snapshot. Read handlers do NOT - // take this lock, so an in-flight update never blocks queries against existing - // snapshots. Lock ordering is updateMu -> snapshotsMu (never the reverse). - updateMu sync.Mutex + languageServerUpdateMu sync.Mutex cpuProfiler pprof.CPUProfiler } @@ -464,7 +450,7 @@ func NewLSPSession(projectSession *project.Session, options *SessionOptions) *Se func NewStandaloneSession(init *project.SessionInit, options *SessionOptions) *Session { snapshotHost := project.NewSnapshotHost(init) s := newSession(snapshotHost, nil, options) - s.compatibilitySnapshot = snapshotHost.NewStandaloneRootSnapshot() + s.ownsSnapshotHost = true return s } @@ -505,25 +491,6 @@ func (s *Session) useCaseSensitiveFileNames() bool { return s.snapshotHost.FS().UseCaseSensitiveFileNames() } -func (s *Session) apiUpdate( - ctx context.Context, - fileChanges project.FileChangeSummary, - apiRequest *project.APISnapshotRequest, -) (*project.Snapshot, error) { - if s.projectSession != nil { - return s.projectSession.APIUpdate(ctx, fileChanges, apiRequest) - } - - s.compatibilityMu.Lock() - defer s.compatibilityMu.Unlock() - oldSnapshot := s.compatibilitySnapshot - snapshot, err := s.snapshotHost.CloneSnapshot(ctx, oldSnapshot, fileChanges, apiRequest) - s.snapshotHost.RetainSnapshot(snapshot) - s.compatibilitySnapshot = snapshot - oldSnapshot.Deref() - return snapshot, err -} - // snapshotHandle creates a snapshot handle from a snapshot's ID. func snapshotHandle(snapshot *project.Snapshot) SnapshotID { return SnapshotID(snapshot.ID()) @@ -552,23 +519,6 @@ func (s *Session) retainSnapshotData(handle SnapshotID) (*snapshotData, error) { return sd, nil } -// retainLatestSnapshotData atomically verifies that handle identifies the latest -// active snapshot and takes a temporary reference that pins it for an update. -// The caller must pair a successful call with releaseSnapshot, including on errors. -func (s *Session) retainLatestSnapshotData(handle SnapshotID) (*snapshotData, error) { - s.snapshotsMu.Lock() - defer s.snapshotsMu.Unlock() - if handle != s.latestSnapshot { - return nil, fmt.Errorf("%w: snapshot %d is not the latest snapshot", ErrClientError, handle) - } - sd := s.snapshots[handle] - if sd == nil { - return nil, fmt.Errorf("%w: snapshot %d not found", ErrClientError, handle) - } - sd.refCount++ - return sd, nil -} - func (s *Session) releaseSnapshot(handle SnapshotID) error { s.snapshotsMu.Lock() sd := s.snapshots[handle] @@ -577,49 +527,14 @@ func (s *Session) releaseSnapshot(handle SnapshotID) error { return fmt.Errorf("%w: snapshot %d not found", ErrClientError, handle) } sd.refCount-- - if sd.refCount > 0 { - s.snapshotsMu.Unlock() - return nil + if sd.refCount <= 0 { + delete(s.snapshots, handle) + sd.snapshot.Deref() } - delete(s.snapshots, snapshotHandle(sd.snapshot)) s.snapshotsMu.Unlock() - - sd.snapshot.Deref() return nil } -func newSnapshotData() *snapshotData { - sd := &snapshotData{ - refCount: 1, - symbolRegistry: make(map[SymbolID]*ast.Symbol), - symbolCanonicalProjects: make(map[SymbolID]ProjectID), - projectRegistries: make(map[ProjectID]*projectRegistryData), - } - return sd -} - -func (s *Session) registerSnapshotData(sd *snapshotData, updateLatest bool) (SnapshotID, *snapshotData) { - handle := snapshotHandle(sd.snapshot) - s.snapshotsMu.Lock() - existingSD := s.snapshots[handle] - if existingSD != nil { - existingSD.refCount++ - } else { - s.snapshots[handle] = sd - } - var previous *snapshotData - if updateLatest { - previous = s.snapshots[s.latestSnapshot] - s.latestSnapshot = handle - } - s.snapshotsMu.Unlock() - - if existingSD != nil { - sd.snapshot.Deref() - } - return handle, previous -} - // checkerSetup holds the common context needed by handlers that require a type checker. type checkerSetup struct { sd *snapshotData @@ -741,8 +656,12 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleRelease(ctx, parsed.(*ReleaseParams)) case string(MethodInitialize): return s.handleInitialize(ctx) + case string(MethodCreateSnapshot): + return s.handleCreateSnapshot(ctx, parsed.(*CreateSnapshotParams)) case string(MethodUpdateSnapshot): return s.handleUpdateSnapshot(ctx, parsed.(*UpdateSnapshotParams)) + case string(MethodGetCurrentLanguageServerSnapshot): + return s.handleGetCurrentLanguageServerSnapshot(ctx, parsed.(*GetCurrentLanguageServerSnapshotParams)) case string(MethodUpdateTemporarySnapshot): return s.handleUpdateTemporarySnapshot(ctx, parsed.(*UpdateTemporarySnapshotParams)) case string(MethodParseCommandLine): @@ -751,8 +670,6 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleReadConfigFile(ctx, parsed.(*ReadConfigFileParams)) case string(MethodParseJsonConfigFile): return s.handleParseJsonConfigFileContent(ctx, parsed.(*ParseJsonConfigFileContentParams)) - case string(MethodCreateProgram): - return s.handleCreateProgram(ctx, parsed.(*CreateProgramParams)) case string(MethodParseConfigFile): return s.handleParseConfigFile(ctx, parsed.(*ParseConfigFileParams)) case string(MethodTranspileModule): @@ -1167,271 +1084,345 @@ func (s *Session) handleInitialize(ctx context.Context) (*InitializeResponse, er }, nil } -// handleUpdateSnapshot creates a new snapshot, optionally opening or closing -// projects and files. With no args, it adopts the latest LSP state. Opens and -// closes are ref-counted per session: the session holds at most one ref per -// project/file, so repeated opens are idempotent and a close only releases a ref -// the session is actually holding. -func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapshotParams) (*UpdateSnapshotResponse, error) { - // Fully serialize updates: snapshot creation, ref tracking, and the - // latestSnapshot/diff bookkeeping must be atomic with respect to other updates, - // otherwise concurrent updates could compute diffs against a non-adjacent - // snapshot or leave latestSnapshot pointing at a stale snapshot. - s.updateMu.Lock() - defer s.updateMu.Unlock() - - var baseSD *snapshotData - if params.Snapshot != 0 { - var err error - baseSD, err = s.retainLatestSnapshotData(params.Snapshot) - if err != nil { - return nil, err - } - // Release only the temporary pin acquired above; the client's Snapshot - // continues to own its existing reference even if this update fails. - defer func() { _ = s.releaseSnapshot(params.Snapshot) }() +// handleCreateSnapshot creates a new independent snapshot. +func (s *Session) handleCreateSnapshot(ctx context.Context, params *CreateSnapshotParams) (*CreateSnapshotResponse, error) { + apiRequest, err := s.toAPISnapshotRequest(¶ms.SnapshotRequestChangesParams) + if err != nil { + return nil, err } + openState := s.reconcileSnapshotOpens(apiRequest, snapshotOpenState{}) fileChanges := s.toFileChangeSummary(params.FileChanges) + var snapshotFileSystem vfs.FS + if params.FileSystem != nil { + fileSystem, fileSystemErr := requestfilesystem.NewForUpdate(params.FileSystem, s.fileSystem(), s.currentDirectory(), &fileChanges) + if fileSystemErr != nil { + return nil, fmt.Errorf("%w: %w", ErrClientError, fileSystemErr) + } + snapshotFileSystem = fileSystem + apiRequest.FileSystem = fileSystem + apiRequest.ReplaceFileSystem = params.FileSystem.Kind == requestfilesystem.KindFull + } + root := s.snapshotHost.NewRootSnapshot() + snapshot, err := s.snapshotHost.CloneSnapshot(ctx, root, fileChanges, apiRequest) + root.Deref() + if err != nil { + snapshot.Deref() + return nil, fmt.Errorf("%w: failed to create snapshot: %w", ErrClientError, err) + } - apiRequest := &project.APISnapshotRequest{} - var baseRequestFileSystem vfs.FS - if baseSD != nil { - baseRequestFileSystem = baseSD.fileSystem + response, err := s.createSnapshotResponse(snapshot, nil, ¶ms.SnapshotRequestChangesParams) + if err != nil { + snapshot.Deref() + return nil, err } - if baseRequestFileSystem == nil && params.FileSystem != nil { - baseRequestFileSystem = s.fileSystem() + s.registerSnapshot(snapshot, openState, snapshotFileSystem) + return response, nil +} + +func (s *Session) handleUpdateSnapshot(ctx context.Context, params *UpdateSnapshotParams) (*CreateSnapshotResponse, error) { + baseSD, err := s.retainSnapshotData(params.Snapshot) + if err != nil { + return nil, err } - sd := newSnapshotData() - var err error - sd.fileSystem, err = requestfilesystem.NewForUpdate(params.FileSystem, baseRequestFileSystem, s.currentDirectory(), &fileChanges) + defer func() { _ = s.releaseSnapshot(params.Snapshot) }() + + changes := params.Changes + if changes == nil { + changes = &CreateSnapshotParams{} + } + apiRequest, err := s.toAPISnapshotRequest(&changes.SnapshotRequestChangesParams) if err != nil { - return nil, fmt.Errorf("%w: %w", ErrClientError, err) + return nil, err } - apiRequest.FileSystem = sd.fileSystem - apiRequest.ReplaceFileSystem = params.FileSystem != nil && params.FileSystem.Kind == requestfilesystem.KindFull + openState := s.reconcileSnapshotOpens(apiRequest, snapshotOpenState{openProjects: baseSD.openProjects, openFiles: baseSD.openFiles}) + fileChanges := s.toFileChangeSummary(changes.FileChanges) + snapshotFileSystem := baseSD.fileSystem + if changes.FileSystem != nil { + baseFileSystem := snapshotFileSystem + if baseFileSystem == nil { + baseFileSystem = s.fileSystem() + } + fileSystem, fileSystemErr := requestfilesystem.NewForUpdate(changes.FileSystem, baseFileSystem, s.currentDirectory(), &fileChanges) + if fileSystemErr != nil { + return nil, fmt.Errorf("%w: %w", ErrClientError, fileSystemErr) + } + snapshotFileSystem = fileSystem + } + if snapshotFileSystem != nil { + apiRequest.FileSystem = snapshotFileSystem + apiRequest.ReplaceFileSystem = changes.FileSystem != nil && changes.FileSystem.Kind == requestfilesystem.KindFull + } + snapshot, err := s.snapshotHost.CloneSnapshot(ctx, baseSD.snapshot, fileChanges, apiRequest) + if err != nil { + snapshot.Deref() + return nil, fmt.Errorf("%w: failed to update snapshot: %w", ErrClientError, err) + } + + response, err := s.createSnapshotResponse(snapshot, baseSD.snapshot, &changes.SnapshotRequestChangesParams) + if err != nil { + snapshot.Deref() + return nil, err + } + s.registerSnapshot(snapshot, openState, snapshotFileSystem) + return response, nil +} + +func (s *Session) toAPISnapshotRequest(changes *SnapshotRequestChangesParams) (*project.APISnapshotRequest, error) { + apiRequest := &project.APISnapshotRequest{} - // Open projects: only take a new ref for projects we aren't already holding open. - var openedProjects []tspath.Path - for _, p := range params.OpenProjects { + for _, p := range changes.OpenProjects { configFileName := p.ToAbsoluteFileName(s.currentDirectory()) - configPath := s.toPath(configFileName) - if s.openProjects.Has(configPath) { - continue + if apiRequest.EnsurePrograms == nil { + apiRequest.EnsurePrograms = collections.NewSetWithSizeHint[tspath.Path](len(changes.OpenProjects)) } + apiRequest.EnsurePrograms.Add(s.toPath(configFileName)) if apiRequest.OpenProjects == nil { - apiRequest.OpenProjects = collections.NewSetWithSizeHint[string](len(params.OpenProjects)) + apiRequest.OpenProjects = collections.NewSetWithSizeHint[string](len(changes.OpenProjects)) } apiRequest.OpenProjects.Add(configFileName) - openedProjects = append(openedProjects, configPath) } - // Close projects: only release a ref we currently hold. - var closedProjects []tspath.Path - for _, p := range params.CloseProjects { + for _, p := range changes.CloseProjects { configPath := s.toPath(p.ToAbsoluteFileName(s.currentDirectory())) - if !s.openProjects.Has(configPath) { - continue - } if apiRequest.CloseProjects == nil { - apiRequest.CloseProjects = collections.NewSetWithSizeHint[tspath.Path](len(params.CloseProjects)) + apiRequest.CloseProjects = collections.NewSetWithSizeHint[tspath.Path](len(changes.CloseProjects)) } apiRequest.CloseProjects.Add(configPath) - closedProjects = append(closedProjects, configPath) } - // Open files: only open files we aren't already holding open, so each file is - // held by at most one API ref from this session. - var openedFiles []tspath.Path - for _, f := range params.OpenFiles { + for _, f := range changes.OpenFiles { uri := f.ToURI(s.currentDirectory()) - path := s.toPath(uri.FileName()) - if s.openFiles.Has(path) { - continue + if apiRequest.EnsureFiles == nil { + apiRequest.EnsureFiles = collections.NewSetWithSizeHint[lsproto.DocumentUri](len(changes.OpenFiles)) } + apiRequest.EnsureFiles.Add(uri) if apiRequest.OpenFiles == nil { - apiRequest.OpenFiles = collections.NewSetWithSizeHint[lsproto.DocumentUri](len(params.OpenFiles)) + apiRequest.OpenFiles = collections.NewSetWithSizeHint[lsproto.DocumentUri](len(changes.OpenFiles)) } apiRequest.OpenFiles.Add(uri) - openedFiles = append(openedFiles, path) } - // Close files: only release a ref we currently hold. - var closedFiles []tspath.Path - for _, f := range params.CloseFiles { + for _, f := range changes.CloseFiles { path := s.toPath(f.ToURI(s.currentDirectory()).FileName()) - if !s.openFiles.Has(path) { - continue - } if apiRequest.CloseFiles == nil { - apiRequest.CloseFiles = collections.NewSetWithSizeHint[tspath.Path](len(params.CloseFiles)) + apiRequest.CloseFiles = collections.NewSetWithSizeHint[tspath.Path](len(changes.CloseFiles)) } apiRequest.CloseFiles.Add(path) - closedFiles = append(closedFiles, path) - } - - // Even when nothing is opened or closed, APIUpdate ensures all projects and - // files opened by the API are up to date. For an API connected to an LSP server, - // this brings the API state up to date with the LSP state and ensures projects - // the API cares about are ready to be queried. - snapshot, err := s.apiUpdate(ctx, fileChanges, apiRequest) - if err != nil { - // APIUpdate returns a ref'd snapshot even on error; release it. - snapshot.Deref() - return nil, fmt.Errorf("%w: failed to update snapshot: %w", ErrClientError, err) } - sd.snapshot = snapshot - // Commit ref tracking now that the update succeeded. - for _, configPath := range openedProjects { - s.openProjects.Add(configPath) - } - for _, configPath := range closedProjects { - s.openProjects.Delete(configPath) - } - for _, path := range openedFiles { - s.openFiles.Add(path) + apiRequest.CreatePrograms = make([]*project.APICreateProgramRequest, len(changes.CreatePrograms)) + for i, programParams := range changes.CreatePrograms { + rootFileNames := make([]string, len(programParams.RootFiles)) + for j, rootFile := range programParams.RootFiles { + rootFileNames[j] = rootFile.ToAbsoluteFileName(s.currentDirectory()) + } + apiRequest.CreatePrograms[i] = &project.APICreateProgramRequest{ + RootFileNames: rootFileNames, + CompilerOptions: &programParams.Options.CompilerOptions, + ProjectReferences: programParams.Options.ProjectReferences, + ConfigFileParsingDiagnostics: core.Map(programParams.Options.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }), + } } - for _, path := range closedFiles { - s.openFiles.Delete(path) + if len(changes.RemovePrograms) > 0 { + apiRequest.RemovePrograms = collections.NewSetWithSizeHint[int](len(changes.RemovePrograms)) } - - // Atomically advance latestSnapshot and retain duplicate handles independently. - handle, prevSD := s.registerSnapshotData(sd, true) - - // Build projects list - projects := snapshot.ProjectCollection.Projects() - projectResponses := make([]*ProjectResponse, 0, len(projects)) - for _, proj := range projects { - if proj.CommandLine == nil { - continue + for _, program := range changes.RemovePrograms { + programID, ok := project.SyntheticProgramID(tspath.Path(program)) + if !ok { + return nil, fmt.Errorf("%w: invalid synthetic project handle: %s", ErrClientError, program) } - projectResponses = append(projectResponses, NewProjectResponse(proj)) + apiRequest.RemovePrograms.Add(programID) } - - // Compute changes from the previous latest snapshot - var changes *SnapshotChanges - if prevSD != nil { - changes = computeSnapshotChanges(prevSD.snapshot, snapshot) + if changes.EnsurePrograms != nil { + apiRequest.EnsureAllPrograms = changes.EnsurePrograms.All + if len(changes.EnsurePrograms.Projects) > 0 && apiRequest.EnsurePrograms == nil { + apiRequest.EnsurePrograms = collections.NewSetWithSizeHint[tspath.Path](len(changes.EnsurePrograms.Projects)) + } + for _, program := range changes.EnsurePrograms.Projects { + apiRequest.EnsurePrograms.Add(parseProjectHandle(program)) + } } + return apiRequest, nil +} - return &UpdateSnapshotResponse{ - Snapshot: handle, - Projects: projectResponses, - Changes: changes, - }, nil +type languageServerSnapshotUpdate struct { + request *project.APISnapshotRequest + openState snapshotOpenState } -// handleUpdateTemporarySnapshot creates a temporary snapshot that overrides the -// content of a single file, without opening/closing any projects or files and -// without advancing the session's latest snapshot. -func (s *Session) handleUpdateTemporarySnapshot(ctx context.Context, params *UpdateTemporarySnapshotParams) (*UpdateSnapshotResponse, error) { - baseSD, err := s.retainSnapshotData(params.Snapshot) +func (s *Session) toLanguageServerSnapshotUpdate(changes *SnapshotRequestChangesParams) (*languageServerSnapshotUpdate, error) { + apiRequest, err := s.toAPISnapshotRequest(changes) if err != nil { return nil, err } - defer func() { _ = s.releaseSnapshot(params.Snapshot) }() - - uri := params.File.ToURI(s.currentDirectory()) - sd := newSnapshotData() - sd.fileSystem = baseSD.fileSystem - - snapshot, err := s.snapshotHost.CloneSnapshotWithTemporaryFile(ctx, baseSD.snapshot, sd.fileSystem, uri, params.NewText) - if err != nil { - return nil, fmt.Errorf("%w: failed to update temporary snapshot: %w", ErrClientError, err) + update := &languageServerSnapshotUpdate{ + request: apiRequest, + openState: s.reconcileSnapshotOpens(apiRequest, snapshotOpenState{ + openProjects: s.openProjects, + openFiles: s.openFiles, + }), } - sd.snapshot = snapshot - handle, _ := s.registerSnapshotData(sd, false) - - // Build projects list - projects := snapshot.ProjectCollection.Projects() - projectResponses := make([]*ProjectResponse, 0, len(projects)) - for _, proj := range projects { - if proj.CommandLine == nil { - continue + for programID := range apiRequest.RemovePrograms.Keys() { + if !s.createdPrograms.Has(programID) { + apiRequest.RemovePrograms.Delete(programID) } - projectResponses = append(projectResponses, NewProjectResponse(proj)) } + return update, nil +} - // Compute changes from the requested base snapshot so the client can retain - // cached source files for unchanged files. - changes := computeSnapshotChanges(baseSD.snapshot, snapshot) +func (u *languageServerSnapshotUpdate) commit(s *Session, snapshot *project.Snapshot) { + s.openProjects = *u.openState.openProjects.Clone() + s.openFiles = *u.openState.openFiles.Clone() + for programID := range u.request.RemovePrograms.Keys() { + s.createdPrograms.Delete(programID) + } + for _, program := range snapshot.CreatedPrograms() { + programID, ok := project.SyntheticProgramID(program.ID()) + if !ok { + panic(fmt.Sprintf("created program has invalid synthetic project path: %s", program.ID())) + } + s.createdPrograms.Add(programID) + } +} - return &UpdateSnapshotResponse{ - Snapshot: handle, - Projects: projectResponses, - Changes: changes, - }, nil +type snapshotOpenState struct { + openProjects collections.Set[tspath.Path] + openFiles collections.Set[tspath.Path] } -func (s *Session) handleCreateProgram(ctx context.Context, params *CreateProgramParams) (*CreateProgramResponse, error) { - if params.FileChanges != nil && params.OldProgram == nil { - return nil, fmt.Errorf("%w: fileChanges requires an oldProgram", ErrClientError) +func (s *Session) reconcileSnapshotOpens(apiRequest *project.APISnapshotRequest, base snapshotOpenState) snapshotOpenState { + state := snapshotOpenState{ + openProjects: *base.openProjects.Clone(), + openFiles: *base.openFiles.Clone(), } - - rootFileNames := make([]string, len(params.RootFiles)) - for i, rootFile := range params.RootFiles { - rootFileNames[i] = rootFile.ToAbsoluteFileName(s.currentDirectory()) + for path := range apiRequest.CloseProjects.Keys() { + if state.openProjects.Has(path) { + state.openProjects.Delete(path) + } else { + apiRequest.CloseProjects.Delete(path) + } } + for configFileName := range apiRequest.OpenProjects.Keys() { + path := s.toPath(configFileName) + if state.openProjects.Has(path) { + apiRequest.OpenProjects.Delete(configFileName) + } else { + state.openProjects.Add(path) + } + } + for path := range apiRequest.CloseFiles.Keys() { + if state.openFiles.Has(path) { + state.openFiles.Delete(path) + } else { + apiRequest.CloseFiles.Delete(path) + } + } + for uri := range apiRequest.OpenFiles.Keys() { + path := s.toPath(uri.FileName()) + if state.openFiles.Has(path) { + apiRequest.OpenFiles.Delete(uri) + } else { + state.openFiles.Add(path) + } + } + return state +} - var oldSnapshot *project.Snapshot - var oldProject *project.Project - var oldFileSystem vfs.FS - if params.OldProgram != nil { - oldSnapshotID := params.OldProgram.Snapshot - oldSD, err := s.retainSnapshotData(oldSnapshotID) - if err != nil { - return nil, err +func (s *Session) registerSnapshot(snapshot *project.Snapshot, openState snapshotOpenState, fileSystem vfs.FS) { + // If the same snapshot ID is returned (no changes), we increment the ref count + // so each client-side Snapshot can be disposed independently. + handle := snapshotHandle(snapshot) + s.snapshotsMu.Lock() + sd, exists := s.snapshots[handle] + if exists { + // Same snapshot already stored — release the caller's ref since + // the stored snapshot already has one, and bump the API refcount. + snapshot.Deref() + sd.refCount++ + } else { + sd = &snapshotData{ + snapshot: snapshot, + fileSystem: fileSystem, + refCount: 1, + openProjects: *openState.openProjects.Clone(), + openFiles: *openState.openFiles.Clone(), + symbolRegistry: make(map[SymbolID]*ast.Symbol), + symbolCanonicalProjects: make(map[SymbolID]ProjectID), + projectRegistries: make(map[ProjectID]*projectRegistryData), } - defer func() { _ = s.releaseSnapshot(oldSnapshotID) }() + s.snapshots[handle] = sd + } + s.snapshotsMu.Unlock() +} - oldSnapshot = oldSD.snapshot - oldFileSystem = oldSD.fileSystem - oldProject, err = oldSD.getProject(params.OldProgram.Project) +func (s *Session) handleGetCurrentLanguageServerSnapshot(ctx context.Context, params *GetCurrentLanguageServerSnapshotParams) (*CreateSnapshotResponse, error) { + if s.projectSession == nil { + return nil, fmt.Errorf("%w: getCurrentLanguageServerSnapshot requires an LSP-connected API session", ErrClientError) + } + var baseSnapshot *project.Snapshot + if params.BaseSnapshot != 0 { + baseSD, err := s.retainSnapshotData(params.BaseSnapshot) if err != nil { return nil, err } + defer func() { _ = s.releaseSnapshot(params.BaseSnapshot) }() + baseSnapshot = baseSD.snapshot } - sd := newSnapshotData() - sd.fileSystem = oldFileSystem - baseSnapshot := oldSnapshot - fileChanges := s.toFileChangeSummary(params.FileChanges) - if baseSnapshot == nil { - var err error - baseSnapshot, err = s.apiUpdate(ctx, fileChanges, nil) - if err != nil { - baseSnapshot.Deref() - return nil, fmt.Errorf("%w: failed to update snapshot: %w", ErrClientError, err) - } - defer baseSnapshot.Deref() - fileChanges = project.FileChangeSummary{} + s.languageServerUpdateMu.Lock() + defer s.languageServerUpdateMu.Unlock() + + changes := params.Changes + if changes == nil { + changes = &LanguageServerSnapshotChanges{} } - snapshot := s.snapshotHost.CloneSnapshotForProgram( - ctx, - baseSnapshot, - sd.fileSystem, - rootFileNames, - ¶ms.CreateProgramOptions.CompilerOptions, - params.CreateProgramOptions.ProjectReferences, - core.Map(params.CreateProgramOptions.ConfigFileParsingDiagnostics, func(d *DiagnosticResponse) *ast.Diagnostic { return d.ToDiagnostic() }), - oldProject, - fileChanges, - ) - project := snapshot.ProjectCollection.InferredProject() - if project == nil { + update, err := s.toLanguageServerSnapshotUpdate(&changes.SnapshotRequestChangesParams) + if err != nil { + return nil, err + } + + snapshot, err := s.projectSession.APIUpdate(ctx, project.FileChangeSummary{}, update.request) + if err != nil { snapshot.Deref() - return nil, fmt.Errorf("%w: failed to create synthetic project", ErrClientError) + return nil, fmt.Errorf("%w: failed to update language server snapshot: %w", ErrClientError, err) } - sd.snapshot = snapshot - handle, _ := s.registerSnapshotData(sd, false) + update.commit(s, snapshot) + response, err := s.createSnapshotResponse(snapshot, baseSnapshot, &changes.SnapshotRequestChangesParams) + if err != nil { + snapshot.Deref() + return nil, err + } + s.registerSnapshot(snapshot, snapshotOpenState{openProjects: s.openProjects, openFiles: s.openFiles}, nil) + return response, nil +} - return &CreateProgramResponse{ - Snapshot: handle, - Project: NewProjectResponse(project), - }, nil +// handleUpdateTemporarySnapshot creates a temporary snapshot that overrides the +// content of a single file, without opening/closing any projects or files and +// without advancing the session's latest snapshot. +func (s *Session) handleUpdateTemporarySnapshot(ctx context.Context, params *UpdateTemporarySnapshotParams) (*CreateSnapshotResponse, error) { + baseSD, err := s.retainSnapshotData(params.Snapshot) + if err != nil { + return nil, err + } + defer func() { _ = s.releaseSnapshot(params.Snapshot) }() + + uri := params.File.ToURI(s.currentDirectory()) + + snapshot, err := s.snapshotHost.CloneSnapshotWithTemporaryFile(ctx, baseSD.snapshot, uri, params.NewText) + if err != nil { + return nil, fmt.Errorf("%w: failed to update temporary snapshot: %w", ErrClientError, err) + } + + response, err := s.createSnapshotResponse(snapshot, baseSD.snapshot, nil) + if err != nil { + snapshot.Deref() + return nil, err + } + s.registerSnapshot(snapshot, snapshotOpenState{openProjects: baseSD.openProjects, openFiles: baseSD.openFiles}, baseSD.fileSystem) + return response, nil } // handleRelease decrements the ref count for a snapshot. @@ -3933,11 +3924,82 @@ func computeSnapshotChanges(prev *project.Snapshot, next *project.Snapshot) *Sna return &changes } +func (s *Session) createSnapshotResponse(snapshot *project.Snapshot, base *project.Snapshot, request *SnapshotRequestChangesParams) (*CreateSnapshotResponse, error) { + operation, err := s.createSnapshotOperationResponse(snapshot, request) + if err != nil { + return nil, err + } + if base == nil { + projects := snapshot.ProjectCollection.Projects() + projectResponses := make([]*ProjectResponse, 0, len(projects)) + for _, proj := range projects { + if proj.CommandLine != nil { + projectResponses = append(projectResponses, NewProjectResponse(proj)) + } + } + return &CreateSnapshotResponse{Snapshot: snapshotHandle(snapshot), Projects: projectResponses, Operation: operation}, nil + } + + projectResponses := make([]*ProjectResponse, 0) + collections.DiffOrderedMaps( + base.ProjectCollection.ProjectsByPath(), snapshot.ProjectCollection.ProjectsByPath(), + func(_ tspath.Path, proj *project.Project) { + if proj.CommandLine != nil { + projectResponses = append(projectResponses, NewProjectResponse(proj)) + } + }, + func(_ tspath.Path, _ *project.Project) {}, + func(_ tspath.Path, oldProj *project.Project, newProj *project.Project) { + if oldProj != newProj && newProj.CommandLine != nil { + projectResponses = append(projectResponses, NewProjectResponse(newProj)) + } + }, + ) + return &CreateSnapshotResponse{ + Snapshot: snapshotHandle(snapshot), + Projects: projectResponses, + Changes: computeSnapshotChanges(base, snapshot), + Operation: operation, + }, nil +} + +func (s *Session) createSnapshotOperationResponse(snapshot *project.Snapshot, request *SnapshotRequestChangesParams) (*SnapshotOperationResponse, error) { + operation := &SnapshotOperationResponse{} + if request == nil { + return operation, nil + } + + if request.CreatePrograms != nil { + createdPrograms := snapshot.CreatedPrograms() + if len(createdPrograms) != len(request.CreatePrograms) { + return nil, fmt.Errorf("%w: created program result count does not match request", ErrClientError) + } + results := make([]SyntheticProjectID, len(createdPrograms)) + for i, createdProgram := range createdPrograms { + results[i] = SyntheticProjectHandle(createdProgram) + } + operation.CreatedPrograms = &results + } + + if request.OpenFiles != nil { + results := make([]*OpenedFileOperationResult, len(request.OpenFiles)) + for i, file := range request.OpenFiles { + project := snapshot.GetDefaultProject(file.ToURI(s.currentDirectory())) + if project == nil { + return nil, fmt.Errorf("%w: no project found for opened file %s", ErrClientError, file.ToAbsoluteFileName(s.currentDirectory())) + } + results[i] = &OpenedFileOperationResult{Project: ProjectHandle(project)} + } + operation.OpenedFiles = &results + } + return operation, nil +} + // Close closes the session and releases all active snapshots, // regardless of their ref counts. func (s *Session) Close() { s.closeOnce.Do(func() { - s.releaseOpenRefs() + s.releaseLanguageServerRefs() s.snapshotsMu.Lock() for handle, sd := range s.snapshots { @@ -3946,25 +4008,21 @@ func (s *Session) Close() { } s.snapshotsMu.Unlock() - if s.projectSession == nil { - if s.compatibilitySnapshot != nil { - s.compatibilitySnapshot.Deref() - s.compatibilitySnapshot = nil - } + if s.ownsSnapshotHost { s.snapshotHost.Close() } s.batchResponsePages.Clear() }) } -// releaseOpenRefs releases every project and file ref this session is holding -// open in a shared project session. Standalone sessions release the entire -// compatibility snapshot when they close, so there is no shared state to update. -func (s *Session) releaseOpenRefs() { - s.updateMu.Lock() - defer s.updateMu.Unlock() +func (s *Session) releaseLanguageServerRefs() { + if s.projectSession == nil { + return + } - if s.openProjects.Len() == 0 && s.openFiles.Len() == 0 { + s.languageServerUpdateMu.Lock() + defer s.languageServerUpdateMu.Unlock() + if s.openProjects.Len() == 0 && s.openFiles.Len() == 0 && s.createdPrograms.Len() == 0 { return } @@ -3975,20 +4033,19 @@ func (s *Session) releaseOpenRefs() { if s.openFiles.Len() > 0 { apiRequest.CloseFiles = s.openFiles.Clone() } - if s.projectSession == nil { - s.openProjects.Clear() - s.openFiles.Clear() - return + if s.createdPrograms.Len() > 0 { + apiRequest.RemovePrograms = collections.NewSetWithSizeHint[int](s.createdPrograms.Len()) + for programID := range s.createdPrograms.Keys() { + apiRequest.RemovePrograms.Add(programID) + } } snapshot, err := s.projectSession.APIUpdate(s.withLocale(context.Background()), project.FileChangeSummary{}, apiRequest) - // APIUpdate returns a ref'd snapshot even on error; always release it. snapshot.Deref() - if err != nil { - return + if err == nil { + s.openProjects.Clear() + s.openFiles.Clear() + s.createdPrograms.Clear() } - - s.openProjects.Clear() - s.openFiles.Clear() } func formatSessionID(id uint64) string { diff --git a/tsc/internal/api/session_apistate_test.go b/tsc/internal/api/session_apistate_test.go index 19696cf0bff98..fcbb90060d96c 100644 --- a/tsc/internal/api/session_apistate_test.go +++ b/tsc/internal/api/session_apistate_test.go @@ -12,284 +12,394 @@ import ( "gotest.tools/v3/assert" ) -func TestStandaloneSessionUsesSnapshotHostWithoutProjectSession(t *testing.T) { +func TestGetCurrentLanguageServerSnapshotAdoptsChanges(t *testing.T) { t.Parallel() if !bundled.Embedded { t.Skip("bundled files are not embedded") } const configFileName = "/home/projects/p/tsconfig.json" - init, _ := projecttestutil.GetSessionInitOptions(map[string]any{ - configFileName: `{ "compilerOptions": { "strict": true } }`, - "/home/projects/p/src/index.ts": `export const x = 1;`, - }, nil, &projecttestutil.TypingsInstallerOptions{}) + const fileName = "/home/projects/p/src/index.ts" + projectSession, utils := projecttestutil.Setup(map[string]any{ + configFileName: `{ "compilerOptions": { "strict": true } }`, + fileName: `export const x = 1;`, + }) + defer projectSession.Close() + + session := NewLSPSession(projectSession, nil) + response, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenProjects: []DocumentIdentifier{{FileName: configFileName}}, + }, + }, + }) + assert.NilError(t, err) + assert.Equal(t, session.openProjects.Len(), 1) + assert.Equal(t, response.Snapshot, snapshotHandle(projectSession.Snapshot())) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil) + assert.NilError(t, utils.FS().WriteFile(fileName, `export const x = 2;`)) + projectSession.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{ + Uri: DocumentIdentifier{FileName: fileName}.ToURI(projectSession.GetCurrentDirectory()), + Type: lsproto.FileChangeTypeChanged, + }}) + dirty, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{BaseSnapshot: response.Snapshot}) + assert.NilError(t, err) + assert.Equal(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)).IsDirty(), true) + + unchanged, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + BaseSnapshot: dirty.Snapshot, + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenProjects: []DocumentIdentifier{{FileName: configFileName}}, + }, + }, + }) + assert.NilError(t, err) + assert.Equal(t, unchanged.Projects[0].Dirty, false) + assert.Equal(t, session.openProjects.Len(), 1) + + removed, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + BaseSnapshot: unchanged.Snapshot, + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CloseProjects: []DocumentIdentifier{{FileName: configFileName}}, + }, + }, + }) + assert.NilError(t, err) + assert.Equal(t, len(removed.Projects), 0) + assert.DeepEqual(t, removed.Changes.RemovedProjects, []ProjectID{ProjectID(configFileName)}) + assert.Equal(t, session.openProjects.Len(), 0) + + session.Close() + assert.Equal(t, session.openProjects.Len(), 0) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil) +} + +func TestGetCurrentLanguageServerSnapshotRejectsStandaloneSession(t *testing.T) { + t.Parallel() + + init, _ := projecttestutil.GetSessionInitOptions(map[string]any{}, nil, &projecttestutil.TypingsInstallerOptions{}) session := NewStandaloneSession(init, nil) defer session.Close() - firstResponse, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: "/home/projects/p/src/index.ts"}}, + _, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{}) + assert.ErrorContains(t, err, "requires an LSP-connected API session") +} + +func TestGetCurrentLanguageServerSnapshotCloseAndReopenProject(t *testing.T) { + t.Parallel() + + const configFileName = "/home/projects/p/tsconfig.json" + projectSession, _ := projecttestutil.Setup(map[string]any{ + configFileName: `{}`, }) + defer projectSession.Close() + ctx := context.Background() + + session := NewLSPSession(projectSession, nil) + defer session.Close() + open := DocumentIdentifier{FileName: configFileName} + _, err := session.handleGetCurrentLanguageServerSnapshot(ctx, &GetCurrentLanguageServerSnapshotParams{Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenProjects: []DocumentIdentifier{open}}, + }}) assert.NilError(t, err) - assert.Equal(t, firstResponse.Snapshot, SnapshotID(1)) - assert.Equal(t, len(firstResponse.Projects), 1) - response, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: configFileName}}, + _, err = session.handleGetCurrentLanguageServerSnapshot(ctx, &GetCurrentLanguageServerSnapshotParams{Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CloseProjects: []DocumentIdentifier{open}, + OpenProjects: []DocumentIdentifier{open}, + }, + }}) + assert.NilError(t, err) + assert.Equal(t, session.openProjects.Len(), 1) + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil) + + _, err = session.handleGetCurrentLanguageServerSnapshot(ctx, &GetCurrentLanguageServerSnapshotParams{Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{CloseProjects: []DocumentIdentifier{open}}, + }}) + assert.NilError(t, err) + assert.Equal(t, session.openProjects.Len(), 0) +} + +func TestGetCurrentLanguageServerSnapshotCloseAndReopenFile(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + "/home/projects/p/tsconfig.json": `{}`, + fileName: `export const value = 1;`, }) + defer projectSession.Close() + ctx := context.Background() + session := NewLSPSession(projectSession, nil) + defer session.Close() + open := DocumentIdentifier{FileName: fileName} + _, err := session.handleGetCurrentLanguageServerSnapshot(ctx, &GetCurrentLanguageServerSnapshotParams{Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenFiles: []DocumentIdentifier{open}}, + }}) assert.NilError(t, err) - assert.Equal(t, response.Snapshot, SnapshotID(2)) - programResponse, err := session.handleCreateProgram(context.Background(), &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: "/home/projects/p/src/index.ts"}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + _, err = session.handleGetCurrentLanguageServerSnapshot(ctx, &GetCurrentLanguageServerSnapshotParams{Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CloseFiles: []DocumentIdentifier{open}, + OpenFiles: []DocumentIdentifier{open}, }, + }}) + assert.NilError(t, err) + assert.Equal(t, session.openFiles.Len(), 1) + assert.Assert(t, projectSession.Snapshot().GetDefaultProject(open.ToURI(projectSession.GetCurrentDirectory())) != nil) + + _, err = session.handleGetCurrentLanguageServerSnapshot(ctx, &GetCurrentLanguageServerSnapshotParams{Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{CloseFiles: []DocumentIdentifier{open}}, + }}) + assert.NilError(t, err) + assert.Equal(t, session.openFiles.Len(), 0) +} + +func TestGetCurrentLanguageServerSnapshotFlushesPendingLSPChanges(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + fileName: `export const value: string = 1;`, }) + defer projectSession.Close() + + projectSession.DidOpenFile( + context.Background(), + DocumentIdentifier{FileName: fileName}.ToURI(projectSession.GetCurrentDirectory()), + 1, + `export const value: string = "ok";`, + lsproto.LanguageKindTypeScript, + ) + + session := NewLSPSession(projectSession, nil) + defer session.Close() + response, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{}) + assert.NilError(t, err) + assert.Equal(t, response.Snapshot, snapshotHandle(projectSession.Snapshot())) + + snapshot, err := session.getSnapshotData(response.Snapshot) assert.NilError(t, err) - assert.Assert(t, programResponse.Project != nil) - assert.Equal(t, programResponse.Snapshot, SnapshotID(4)) + assert.Equal(t, snapshot.snapshot.GetFile(fileName).Content(), `export const value: string = "ok";`) } -// TestSessionTracksAndReleasesAPIRefs verifies that an API session holds at most -// one ref per opened project/file (opens are idempotent) and releases exactly -// those refs when the session is closed, so it never leaks or over-releases refs -// in the underlying (potentially shared) project session. -func TestSessionTracksAndReleasesAPIRefs(t *testing.T) { +func TestGetCurrentLanguageServerSnapshotReportsOpenedFilesInRequestOrder(t *testing.T) { t.Parallel() - if !bundled.Embedded { - t.Skip("bundled files are not embedded") - } - t.Run("project opens are idempotent and released on close", func(t *testing.T) { - t.Parallel() - const configFileName = "/home/projects/p/tsconfig.json" - files := map[string]any{ - configFileName: `{ "compilerOptions": { "strict": true } }`, - "/home/projects/p/src/index.ts": `export const x = 1;`, - } - projectSession, _ := projecttestutil.Setup(files) - defer projectSession.Close() - session := NewLSPSession(projectSession, nil) - assert.Assert(t, session.compatibilitySnapshot == nil) - - _, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: configFileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openProjects.Len(), 1) - - // Opening the same project again must not take an additional ref. - _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: configFileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openProjects.Len(), 1) - - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil) - - // Closing the session releases the single API ref, so the project is no - // longer kept loaded. - session.Close() - assert.Equal(t, session.openProjects.Len(), 0) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil) + const configuredFile = "/home/projects/p/index.ts" + const inferredFile = "/home/projects/loose.ts" + projectSession, utils := projecttestutil.Setup(map[string]any{ + "/home/projects/p/tsconfig.json": `{}`, + configuredFile: `export const configured = 1;`, + inferredFile: `export const inferred = 1;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + changes := &LanguageServerSnapshotChanges{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenFiles: []DocumentIdentifier{{FileName: inferredFile}, {FileName: configuredFile}}, + }} + first, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{Changes: changes}) + assert.NilError(t, err) + assert.Equal(t, len(*first.Operation.OpenedFiles), 2) + assert.Equal(t, (*first.Operation.OpenedFiles)[0].Project, ProjectID("/dev/null/inferred")) + assert.Equal(t, (*first.Operation.OpenedFiles)[1].Project, ProjectID("/home/projects/p/tsconfig.json")) + assert.NilError(t, utils.FS().WriteFile(configuredFile, `export const configured = 2;`)) + projectSession.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{ + Uri: DocumentIdentifier{FileName: configuredFile}.ToURI(projectSession.GetCurrentDirectory()), + Type: lsproto.FileChangeTypeChanged, + }}) + dirty, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{BaseSnapshot: first.Snapshot}) + assert.NilError(t, err) + assert.Equal(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/p/tsconfig.json")).IsDirty(), true) + + reopened, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + BaseSnapshot: dirty.Snapshot, + Changes: changes, }) + assert.NilError(t, err) + assert.DeepEqual(t, *reopened.Operation.OpenedFiles, *first.Operation.OpenedFiles) + assert.Equal(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/p/tsconfig.json")).IsDirty(), false) + assert.Equal(t, session.openFiles.Len(), 2) +} + +func TestGetCurrentLanguageServerSnapshotCreatesAndRemovesPrograms(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{fileName: `export const value = 1;`}) + defer projectSession.Close() - t.Run("explicit close releases the project ref", func(t *testing.T) { - t.Parallel() - const configFileName = "/home/projects/p/tsconfig.json" - files := map[string]any{ - configFileName: `{ "compilerOptions": { "strict": true } }`, - "/home/projects/p/src/index.ts": `export const x = 1;`, - } - projectSession, _ := projecttestutil.Setup(files) - defer projectSession.Close() - session := NewLSPSession(projectSession, nil) - defer session.Close() - - _, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: configFileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openProjects.Len(), 1) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil) - - // Closing a project we hold releases the ref and unloads the project. - _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - CloseProjects: []DocumentIdentifier{{FileName: configFileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openProjects.Len(), 0) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil) - - // Closing a project we don't hold is a no-op (never over-releases). - _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - CloseProjects: []DocumentIdentifier{{FileName: configFileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openProjects.Len(), 0) + session := NewLSPSession(projectSession, nil) + defer session.Close() + created, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + Options: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }, + }}, + }, + }, }) + assert.NilError(t, err) + assert.Equal(t, len(created.Projects), 1) + assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 1) - t.Run("file opens are idempotent and released on close", func(t *testing.T) { - t.Parallel() - const fileName = "/home/projects/p/src/index.ts" - files := map[string]any{ - "/home/projects/p/tsconfig.json": `{ "compilerOptions": { "strict": true } }`, - fileName: `export const x = 1;`, - } - projectSession, _ := projecttestutil.Setup(files) - defer projectSession.Close() - session := NewLSPSession(projectSession, nil) - - _, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: fileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openFiles.Len(), 1) - - // Re-opening the same file must not take an additional ref. - _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: fileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openFiles.Len(), 1) - - // The file should resolve to the configured project via ancestor search. - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/p/tsconfig.json")) != nil) - - // Closing a file we don't hold is a no-op (never over-releases). - _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - CloseFiles: []DocumentIdentifier{{FileName: "/home/projects/p/other.ts"}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openFiles.Len(), 1) - - // Explicitly closing the held file releases the ref. - _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - CloseFiles: []DocumentIdentifier{{FileName: fileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openFiles.Len(), 0) - - // Closing the file also tears down the configured project that was - // auto-loaded to serve it, instead of leaking it. - assert.Assert(t, - projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/p/tsconfig.json")) == nil, - "configured project auto-loaded for the API-opened file should be unloaded after close", - ) - - session.Close() - assert.Equal(t, session.openFiles.Len(), 0) + removed, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{RemovePrograms: []SyntheticProjectID{SyntheticProjectID(created.Projects[0].Id), SyntheticProjectID(created.Projects[0].Id)}}, + }, }) + assert.NilError(t, err) + assert.Equal(t, len(removed.Projects), 0) + assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 0) +} - t.Run("relative file paths normalize consistently for open and close", func(t *testing.T) { - t.Parallel() - // The project session's current directory is "/", so a relative path - // resolves to the corresponding absolute path. - files := map[string]any{ - "/src/tsconfig.json": `{ "compilerOptions": { "strict": true } }`, - "/src/index.ts": `export const x = 1;`, - } - projectSession, _ := projecttestutil.Setup(files) - defer projectSession.Close() - session := NewLSPSession(projectSession, nil) - defer session.Close() - - // Open via a relative path; it should be tracked under the absolute path - // and resolve to the containing configured project. - openResp, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: "src/index.ts"}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openFiles.Len(), 1) - assert.Assert(t, session.openFiles.Has(tspath.Path("/src/index.ts"))) - assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) != nil) - - // getDefaultProjectForFile must also resolve a relative path to the same - // configured project (it builds a URI from the identifier internally). - proj, err := session.handleGetDefaultProjectForFile(context.Background(), &GetDefaultProjectForFileParams{ - Snapshot: openResp.Snapshot, - File: DocumentIdentifier{FileName: "src/index.ts"}, - }) - assert.NilError(t, err) - assert.Assert(t, proj != nil, "relative path should resolve to a default project") - assert.Equal(t, proj.ConfigFileName, "/src/tsconfig.json") - - // Re-opening via the absolute path must match the relative open (no new ref). - _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: "/src/index.ts"}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openFiles.Len(), 1) - - // Closing via a relative path must match the path stored when opening. - _, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - CloseFiles: []DocumentIdentifier{{FileName: "src/index.ts"}}, - }) - assert.NilError(t, err) - assert.Equal(t, session.openFiles.Len(), 0) - assert.Assert(t, - projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path("/src/tsconfig.json")) == nil, - "configured project should be unloaded after closing the relatively-pathed file", - ) +func TestClosingAPISessionRemovesCreatedLanguageServerPrograms(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{fileName: `export const value = 1;`}) + defer projectSession.Close() + + session := NewLSPSession(projectSession, nil) + _, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + Options: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }, + }}, + }, + }, }) + assert.NilError(t, err) + assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 1) + + session.Close() + assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 0) } -// TestUpdateSnapshotResponseSkipsUnloadedAncestorProject verifies that API -// updateSnapshot does not report unloaded ancestor project placeholders. This -// covers the case where opening a file loads its nearest configured project -// while solution search discovers an ancestor tsconfig placeholder whose command -// line is still nil. -func TestUpdateSnapshotResponseSkipsUnloadedAncestorProject(t *testing.T) { +func TestLanguageServerProgramOwnershipIsIsolatedByAPISession(t *testing.T) { t.Parallel() - if !bundled.Embedded { - t.Skip("bundled files are not embedded") - } - const ( - nestedConfigFileName = "/repo/packages/app/tsconfig.json" - ancestorConfigFileName = "/repo/packages/tsconfig.json" - fileName = "/repo/packages/app/src/index.ts" - ) - files := map[string]any{ - ancestorConfigFileName: `{ "files": [] }`, - nestedConfigFileName: `{ - "compilerOptions": { "composite": true }, - "include": ["**/*"] - }`, - fileName: `let s: string = 1234;`, - } - projectSession, _ := projecttestutil.Setup(files) + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{fileName: `export const value = 1;`}) defer projectSession.Close() - projectSession.DidOpenFile(context.Background(), lsproto.DocumentUri("file://"+fileName), 1, files[fileName].(string), lsproto.LanguageKindTypeScript) - snapshot := projectSession.Snapshot() - nestedProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path(nestedConfigFileName)) - assert.Assert(t, nestedProject != nil) - assert.Assert(t, nestedProject.CommandLine != nil) - ancestorProject := snapshot.ProjectCollection.ConfiguredProject(tspath.Path(ancestorConfigFileName)) - assert.Assert(t, ancestorProject != nil) - assert.Assert(t, ancestorProject.CommandLine == nil) + owner := NewLSPSession(projectSession, nil) + created, err := owner.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + Options: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }, + }}, + }, + }, + }) + assert.NilError(t, err) + + other := NewLSPSession(projectSession, nil) + _, err = other.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{RemovePrograms: []SyntheticProjectID{SyntheticProjectID(created.Projects[0].Id)}}, + }, + }) + assert.NilError(t, err) + assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 1) + + other.Close() + assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 1) + owner.Close() + assert.Equal(t, len(projectSession.Snapshot().ProjectCollection.SyntheticProjects()), 0) +} + +func TestOpeningProjectOwnedByAnotherAPISessionEnsuresProgram(t *testing.T) { + t.Parallel() + + const configFileName = "/home/projects/p/tsconfig.json" + const fileName = "/home/projects/p/index.ts" + projectSession, utils := projecttestutil.Setup(map[string]any{ + configFileName: `{}`, + fileName: `export const value = 1;`, + }) + defer projectSession.Close() + openProject := &LanguageServerSnapshotChanges{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenProjects: []DocumentIdentifier{{FileName: configFileName}}, + }} + + owner := NewLSPSession(projectSession, nil) + _, err := owner.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{Changes: openProject}) + assert.NilError(t, err) + assert.NilError(t, utils.FS().WriteFile(fileName, `export const value = 2;`)) + projectSession.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{ + Uri: DocumentIdentifier{FileName: fileName}.ToURI(projectSession.GetCurrentDirectory()), + Type: lsproto.FileChangeTypeChanged, + }}) + _, err = owner.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{}) + assert.NilError(t, err) + assert.Equal(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)).IsDirty(), true) + + other := NewLSPSession(projectSession, nil) + opened, err := other.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{Changes: openProject}) + assert.NilError(t, err) + assert.Equal(t, opened.Projects[0].Dirty, false) + assert.Equal(t, other.openProjects.Len(), 1) + + other.Close() + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) != nil) + owner.Close() + assert.Assert(t, projectSession.Snapshot().ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil) +} + +func TestGetCurrentLanguageServerSnapshotOpeningLSPFileEnsuresConfiguredProgram(t *testing.T) { + t.Parallel() + + const configFileName = "/home/projects/p/tsconfig.json" + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + configFileName: `{}`, + fileName: `export const value = 1;`, + }) + defer projectSession.Close() + uri := DocumentIdentifier{FileName: fileName}.ToURI(projectSession.GetCurrentDirectory()) + projectSession.DidOpenFile(context.Background(), uri, 1, `export const value = 1;`, lsproto.LanguageKindTypeScript) session := NewLSPSession(projectSession, nil) defer session.Close() + initial, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{}) + assert.NilError(t, err) + assert.Equal(t, initial.Projects[0].Dirty, false) + projectID := initial.Projects[0].Id - response, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: nestedConfigFileName}}, + projectSession.DidChangeFile(context.Background(), uri, 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{{ + WholeDocument: &lsproto.TextDocumentContentChangeWholeDocument{Text: `export const value = 2;`}, + }}) + dirty, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{BaseSnapshot: initial.Snapshot}) + assert.NilError(t, err) + assert.Equal(t, dirty.Projects[0].Dirty, true) + + ensured, err := session.handleGetCurrentLanguageServerSnapshot(context.Background(), &GetCurrentLanguageServerSnapshotParams{ + BaseSnapshot: dirty.Snapshot, + Changes: &LanguageServerSnapshotChanges{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + }, + }, }) assert.NilError(t, err) - - var foundNestedProject bool - var foundAncestorProject bool - for _, project := range response.Projects { - switch project.ConfigFileName { - case nestedConfigFileName: - foundNestedProject = true - assert.Assert(t, project.RootFiles != nil) - assert.Assert(t, project.CompilerOptions != nil) - case ancestorConfigFileName: - foundAncestorProject = true - } - } - assert.Assert(t, foundNestedProject) - assert.Assert(t, !foundAncestorProject) + assert.Equal(t, ensured.Projects[0].Dirty, false) + assert.Equal(t, (*ensured.Operation.OpenedFiles)[0].Project, projectID) } diff --git a/tsc/internal/api/session_completion_test.go b/tsc/internal/api/session_completion_test.go index 738ce811835cf..4da15ed6dc87e 100644 --- a/tsc/internal/api/session_completion_test.go +++ b/tsc/internal/api/session_completion_test.go @@ -42,8 +42,10 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + snapshotResp, err := session.handleCreateSnapshot(t.Context(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + }, }) assert.NilError(t, err) @@ -114,8 +116,10 @@ func TestCompletionOnInferredProject(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + snapshotResp, err := session.handleCreateSnapshot(t.Context(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + }, }) assert.NilError(t, err) @@ -160,8 +164,10 @@ func TestCompletionRetriesWithAutoImports(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + snapshotResp, err := session.handleCreateSnapshot(t.Context(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + }, }) assert.NilError(t, err) proj, err := session.handleGetDefaultProjectForFile(t.Context(), &GetDefaultProjectForFileParams{ @@ -209,8 +215,8 @@ func TestCompletionWithSymbolsAndExistingImportDoesNotDeadlock(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + snapshotResp, err := session.handleCreateSnapshot(t.Context(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenFiles: []DocumentIdentifier{{FileName: fileName}}}, }) assert.NilError(t, err) proj, err := session.handleGetDefaultProjectForFile(t.Context(), &GetDefaultProjectForFileParams{ diff --git a/tsc/internal/api/session_createprogram_test.go b/tsc/internal/api/session_createprogram_test.go index 2511bdaf11779..0621af7af110d 100644 --- a/tsc/internal/api/session_createprogram_test.go +++ b/tsc/internal/api/session_createprogram_test.go @@ -4,205 +4,48 @@ import ( "context" "testing" + "github.com/microsoft/TypeScript/tsc/internal/bundled" "github.com/microsoft/TypeScript/tsc/internal/core" - "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" - "github.com/microsoft/TypeScript/tsc/internal/project" + "github.com/microsoft/TypeScript/tsc/internal/json" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) -func TestCreateProgram(t *testing.T) { +func TestCreateSnapshotUsesIndependentRoots(t *testing.T) { t.Parallel() + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } - const fileName = "/home/projects/p/index.ts" - projectSession, sessionUtils := projecttestutil.Setup(map[string]any{ - fileName: `export const value: string = 1;`, - }) - defer projectSession.Close() - - session := NewLSPSession(projectSession, nil) - defer session.Close() - ctx := context.Background() - - baseResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{}) - assert.NilError(t, err) - projectSession.DidOpenFile( - ctx, - DocumentIdentifier{FileName: fileName}.ToURI(projectSession.GetCurrentDirectory()), - 1, - `export const value: string = "valid overlay";`, - lsproto.LanguageKindTypeScript, - ) - - response, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Strict: core.TSTrue, - }, - }, - }) - assert.NilError(t, err) - assert.Assert(t, response.Snapshot != baseResponse.Snapshot) - assert.Equal(t, response.Snapshot, SnapshotID(4)) - assert.Equal(t, session.latestSnapshot, baseResponse.Snapshot) - assert.Assert(t, response.Project != nil) - assert.DeepEqual(t, response.Project.RootFiles, []string{fileName}) - assert.Equal(t, response.Project.CompilerOptions.Strict, core.TSTrue) - - snapshot, err := session.getSnapshotData(response.Snapshot) - assert.NilError(t, err) - assert.Equal(t, len(snapshot.snapshot.ProjectCollection.Projects()), 1) - - diagnostics, err := session.handleGetSemanticDiagnostics(ctx, &GetDiagnosticsParams{ - Snapshot: response.Snapshot, - Project: response.Project.Id, - Files: []DocumentIdentifier{{FileName: fileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, len(diagnostics), 0) - - assert.NilError(t, sessionUtils.FS().WriteFile(fileName, `export const value: string = "valid on disk";`)) - updatedResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Strict: core.TSTrue, - }, - }, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: response.Snapshot, - Project: response.Project.Id, - }, - FileChanges: &APIFileChanges{ - Changed: []DocumentIdentifier{{FileName: fileName}}, - }, - }) - assert.NilError(t, err) - updatedSnapshot, err := session.getSnapshotData(updatedResponse.Snapshot) - assert.NilError(t, err) - updatedProject := updatedSnapshot.snapshot.ProjectCollection.InferredProject() - assert.Assert(t, updatedProject != nil) - - updatedDiagnostics, err := session.handleGetSemanticDiagnostics(ctx, &GetDiagnosticsParams{ - Snapshot: updatedResponse.Snapshot, - Project: updatedResponse.Project.Id, - Files: []DocumentIdentifier{{FileName: fileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, len(updatedDiagnostics), 0) - - oldDiagnostics, err := session.handleGetSemanticDiagnostics(ctx, &GetDiagnosticsParams{ - Snapshot: response.Snapshot, - Project: response.Project.Id, - Files: []DocumentIdentifier{{FileName: fileName}}, - }) - assert.NilError(t, err) - assert.Equal(t, len(oldDiagnostics), 0) - - _, err = session.handleRelease(ctx, &ReleaseParams{Snapshot: updatedResponse.Snapshot}) - assert.NilError(t, err) - _, err = session.handleRelease(ctx, &ReleaseParams{Snapshot: response.Snapshot}) - assert.NilError(t, err) - _, err = session.getSnapshotData(response.Snapshot) - assert.ErrorContains(t, err, "not found") - _, err = session.getSnapshotData(baseResponse.Snapshot) - assert.NilError(t, err) -} - -func TestCreateProgramWithNoRootFiles(t *testing.T) { - t.Parallel() - - projectSession, _ := projecttestutil.Setup(map[string]any{}) - defer projectSession.Close() - - session := NewLSPSession(projectSession, nil) - defer session.Close() - - response, err := session.handleCreateProgram(context.Background(), &CreateProgramParams{ - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, - }, - }) - assert.NilError(t, err) - assert.Assert(t, response.Project != nil) - assert.Equal(t, len(response.Project.RootFiles), 0) - - snapshot, err := session.getSnapshotData(response.Snapshot) - assert.NilError(t, err) - project := snapshot.snapshot.ProjectCollection.InferredProject() - assert.Assert(t, project != nil) - assert.Assert(t, project.Program != nil) - assert.Equal(t, len(project.Program.GetSourceFiles()), 0) -} - -func TestCreateProgramFileChangesRequireOldProgram(t *testing.T) { - t.Parallel() - - projectSession, _ := projecttestutil.Setup(map[string]any{}) - defer projectSession.Close() - - session := NewLSPSession(projectSession, nil) - defer session.Close() - - _, err := session.handleCreateProgram(context.Background(), &CreateProgramParams{ - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, - }, - FileChanges: &APIFileChanges{InvalidateAll: true}, - }) - assert.ErrorContains(t, err, "fileChanges requires an oldProgram") -} - -func TestCreateProgramRemovesAllRootFiles(t *testing.T) { - t.Parallel() - - const fileName = "/home/projects/p/index.ts" - projectSession, _ := projecttestutil.Setup(map[string]any{ - fileName: "export {};", - }) - defer projectSession.Close() - - session := NewLSPSession(projectSession, nil) + init, _ := projecttestutil.GetSessionInitOptions(map[string]any{ + "/home/projects/p/src/index.ts": `export const x = 1;`, + }, nil, &projecttestutil.TypingsInstallerOptions{}) + session := NewStandaloneSession(init, nil) defer session.Close() - ctx := context.Background() - - oldResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, - }, - }) - assert.NilError(t, err) - response, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, - }, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: oldResponse.Snapshot, - Project: oldResponse.Project.Id, - }, - FileChanges: &APIFileChanges{ - Changed: []DocumentIdentifier{{FileName: fileName}}, + firstResponse, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: "/home/projects/p/src/index.ts"}}, + Options: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }, + }}, }, }) assert.NilError(t, err) - assert.Assert(t, response.Project != nil) - assert.Equal(t, len(response.Project.RootFiles), 0) + assert.Equal(t, firstResponse.Snapshot, SnapshotID(1)) + assert.Equal(t, len(firstResponse.Projects), 1) - snapshot, err := session.getSnapshotData(response.Snapshot) + response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{}) assert.NilError(t, err) - project := snapshot.snapshot.ProjectCollection.InferredProject() - assert.Assert(t, project != nil) - assert.Assert(t, project.Program != nil) - assert.Equal(t, len(project.Program.GetSourceFiles()), 0) + assert.Equal(t, response.Snapshot, SnapshotID(2)) + assert.Equal(t, len(response.Projects), 0) + assert.Equal(t, len(firstResponse.Projects), 1) } -func TestCreateProgramPreservesRootFileOrder(t *testing.T) { +func TestCreateSnapshotCreatesPrograms(t *testing.T) { t.Parallel() const ( @@ -217,262 +60,122 @@ func TestCreateProgramPreservesRootFileOrder(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - ctx := context.Background() - oldResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileB}, {FileName: fileA}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, - }, - }) - assert.NilError(t, err) - assert.DeepEqual(t, oldResponse.Project.RootFiles, []string{fileB, fileA}) - - response, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileA}, {FileName: fileB}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, - }, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: oldResponse.Snapshot, - Project: oldResponse.Project.Id, + response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{ + { + RootFiles: []DocumentIdentifier{{FileName: fileA}, {FileName: fileB}}, + Options: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue, Strict: core.TSTrue}, + }, + }, + { + RootFiles: []DocumentIdentifier{{FileName: fileB}}, + Options: CreateProgramOptions{ + CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}, + }, + }, + }, }, }) assert.NilError(t, err) - assert.DeepEqual(t, response.Project.RootFiles, []string{fileA, fileB}) + assert.Equal(t, len(response.Projects), 2) + assert.DeepEqual(t, *response.Operation.CreatedPrograms, []SyntheticProjectID{"/dev/null/synthetic/1", "/dev/null/synthetic/2"}) + assert.DeepEqual(t, response.Projects[0].RootFiles, []string{fileA, fileB}) + assert.Equal(t, response.Projects[0].CompilerOptions.Strict, core.TSTrue) + assert.DeepEqual(t, response.Projects[1].RootFiles, []string{fileB}) snapshot, err := session.getSnapshotData(response.Snapshot) assert.NilError(t, err) - assert.Equal(t, snapshot.snapshot.ProjectCollection.InferredProject().ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) + assert.Equal(t, len(snapshot.snapshot.ProjectCollection.SyntheticProjects()), 2) + for _, projectResponse := range response.Projects { + assert.Assert(t, snapshot.snapshot.ProjectCollection.GetProjectByPath(tspath.Path(projectResponse.Id)) != nil) + } } -func TestCreateProgramReusesProgram(t *testing.T) { +func TestSnapshotOperationResponseOmitsUnrequestedFields(t *testing.T) { t.Parallel() - const fileName = "/home/projects/p/index.ts" - projectSession, sessionUtils := projecttestutil.Setup(map[string]any{ - fileName: `export const value: string = 1;`, - }) + projectSession, _ := projecttestutil.Setup(map[string]any{}) defer projectSession.Close() - session := NewLSPSession(projectSession, nil) defer session.Close() - ctx := context.Background() - - oldResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Strict: core.TSTrue, - }, - }, - }) - assert.NilError(t, err) - assert.NilError(t, sessionUtils.FS().WriteFile(fileName, `export const value: string = "valid";`)) - updatedResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Strict: core.TSTrue, - }, - }, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: oldResponse.Snapshot, - Project: oldResponse.Project.Id, - }, - FileChanges: &APIFileChanges{ - Changed: []DocumentIdentifier{{FileName: fileName}}, - }, - }) + response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{}) assert.NilError(t, err) - - updatedSnapshot, err := session.getSnapshotData(updatedResponse.Snapshot) + encoded, err := json.Marshal(response.Operation) assert.NilError(t, err) - updatedProject := updatedSnapshot.snapshot.ProjectCollection.InferredProject() - assert.Assert(t, updatedProject != nil) - assert.Equal(t, updatedProject.ProgramUpdateKind, project.ProgramUpdateKindCloned) + assert.Equal(t, string(encoded), `{}`) - changedOptionsResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Strict: core.TSFalse, - }, - }, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: oldResponse.Snapshot, - Project: oldResponse.Project.Id, + response, err = session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{}, + OpenFiles: []DocumentIdentifier{}, }, }) assert.NilError(t, err) - changedOptionsSnapshot, err := session.getSnapshotData(changedOptionsResponse.Snapshot) + encoded, err = json.Marshal(response.Operation) assert.NilError(t, err) - changedOptionsProject := changedOptionsSnapshot.snapshot.ProjectCollection.InferredProject() - assert.Assert(t, changedOptionsProject != nil) - assert.Equal(t, changedOptionsProject.CommandLine.CompilerOptions().Strict, core.TSFalse) - assert.Equal(t, changedOptionsProject.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) + assert.Equal(t, string(encoded), `{"createdPrograms":[],"openedFiles":[]}`) } -func TestCreateProgramProjectReferencesAndReuse(t *testing.T) { +func TestCreateSnapshotRejectsRemovingProgramFromIndependentRoot(t *testing.T) { t.Parallel() - const ( - fileName = "/home/projects/app/index.ts" - libConfigName = "/home/projects/lib/tsconfig.json" - otherConfigName = "/home/projects/other/tsconfig.json" - ) - projectSession, sessionUtils := projecttestutil.Setup(map[string]any{ - fileName: `export const value: string = 1;`, - libConfigName: `{ "compilerOptions": { "composite": true, "noLib": true }, "files": ["index.ts"] }`, - "/home/projects/lib/index.ts": `export const lib = 1;`, - otherConfigName: `{ "compilerOptions": { "composite": true, "noLib": true }, "files": ["index.ts"] }`, - "/home/projects/other/index.ts": `export const other = 1;`, - }) + projectSession, _ := projecttestutil.Setup(map[string]any{}) defer projectSession.Close() session := NewLSPSession(projectSession, nil) defer session.Close() - ctx := context.Background() - libReference := &core.ProjectReference{Path: libConfigName, OriginalPath: libConfigName} - - oldResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue, Strict: core.TSTrue}, - ProjectReferences: []*core.ProjectReference{libReference}, - }, - }) - assert.NilError(t, err) - assert.DeepEqual(t, oldResponse.Project.ParsedCommandLine.ProjectReferences, []*core.ProjectReference{libReference}) - oldSnapshot, err := session.getSnapshotData(oldResponse.Snapshot) - assert.NilError(t, err) - resolvedReferences := oldSnapshot.snapshot.ProjectCollection.InferredProject().Program.GetResolvedProjectReferences() - assert.Equal(t, len(resolvedReferences), 1) - assert.Equal(t, resolvedReferences[0].ConfigName(), libConfigName) - assert.NilError(t, sessionUtils.FS().WriteFile(fileName, `export const value: string = "valid";`)) - equivalentLibReference := &core.ProjectReference{Path: libConfigName, OriginalPath: "../lib"} - reusedResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue, Strict: core.TSTrue}, - ProjectReferences: []*core.ProjectReference{equivalentLibReference}, + _, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + RemovePrograms: []SyntheticProjectID{"/dev/null/synthetic/1"}, }, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: oldResponse.Snapshot, - Project: oldResponse.Project.Id, - }, - FileChanges: &APIFileChanges{Changed: []DocumentIdentifier{{FileName: fileName}}}, }) - assert.NilError(t, err) - reusedSnapshot, err := session.getSnapshotData(reusedResponse.Snapshot) - assert.NilError(t, err) - assert.Equal(t, reusedSnapshot.snapshot.ProjectCollection.InferredProject().ProgramUpdateKind, project.ProgramUpdateKindCloned) - - otherReference := &core.ProjectReference{Path: otherConfigName, OriginalPath: otherConfigName} - changedResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: fileName}}, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue, Strict: core.TSTrue}, - ProjectReferences: []*core.ProjectReference{otherReference}, - }, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: oldResponse.Snapshot, - Project: oldResponse.Project.Id, - }, - }) - assert.NilError(t, err) - changedSnapshot, err := session.getSnapshotData(changedResponse.Snapshot) - assert.NilError(t, err) - changedProject := changedSnapshot.snapshot.ProjectCollection.InferredProject() - assert.Equal(t, changedProject.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) - assert.DeepEqual(t, changedProject.CommandLine.ProjectReferences(), []*core.ProjectReference{otherReference}) + assert.ErrorContains(t, err, "synthetic program not found for removal: 1") } -func TestCreateProgramFromConfiguredProgramDoesNotRetainOtherProjects(t *testing.T) { +func TestUpdateSnapshotEnsuresSyntheticProgram(t *testing.T) { t.Parallel() - const ( - configFileName = "/home/projects/p/tsconfig.json" - fileName = "/home/projects/p/index.ts" - otherConfigFileName = "/home/projects/other/tsconfig.json" - otherFileName = "/home/projects/other/index.ts" - ) - projectSession, sessionUtils := projecttestutil.Setup(map[string]any{ - configFileName: `{ "compilerOptions": { "noLib": true, "strict": true }, "files": ["index.ts"] }`, - fileName: `export const value: string = 1;`, - otherConfigFileName: `{ "files": ["index.ts"] }`, - otherFileName: `export const other = 1;`, - }) + const fileName = "/home/projects/p/index.ts" + projectSession, utils := projecttestutil.Setup(map[string]any{fileName: `export const value = 1;`}) defer projectSession.Close() - session := NewLSPSession(projectSession, nil) defer session.Close() - ctx := context.Background() - - baseResponse, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: configFileName}, {FileName: otherConfigFileName}}, - }) - assert.NilError(t, err) - var baseProject *ProjectResponse - for _, candidate := range baseResponse.Projects { - if candidate.ConfigFileName == configFileName { - baseProject = candidate - break - } - } - assert.Assert(t, baseProject != nil) - rootFiles := make([]DocumentIdentifier, len(baseProject.RootFiles)) - for i, rootFile := range baseProject.RootFiles { - rootFiles[i] = DocumentIdentifier{FileName: rootFile} - } - assert.NilError(t, sessionUtils.FS().WriteFile(fileName, `export const value: string = "valid";`)) - updatedResponse, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: rootFiles, - CreateProgramOptions: CreateProgramOptions{ - CompilerOptions: core.CompilerOptions{ - NoLib: core.TSTrue, - Strict: core.TSTrue, - }, - }, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: baseResponse.Snapshot, - Project: baseProject.Id, - }, - FileChanges: &APIFileChanges{ - Changed: []DocumentIdentifier{{FileName: fileName}}, + created, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: fileName}}, + Options: CreateProgramOptions{CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}}, + }}, }, }) assert.NilError(t, err) + assert.Equal(t, created.Projects[0].Dirty, false) + projectID := created.Projects[0].Id - updatedSnapshot, err := session.getSnapshotData(updatedResponse.Snapshot) - assert.NilError(t, err) - assert.Equal(t, len(updatedSnapshot.snapshot.ProjectCollection.Projects()), 1) - assert.Equal(t, len(updatedSnapshot.snapshot.ProjectCollection.ConfiguredProjects()), 0) - assert.Assert(t, updatedSnapshot.snapshot.ConfigFileRegistry.GetConfig(tspath.Path(otherConfigFileName)) == nil) - updatedProject := updatedSnapshot.snapshot.ProjectCollection.InferredProject() - assert.Assert(t, updatedProject != nil) - assert.Equal(t, updatedProject.ProgramUpdateKind, project.ProgramUpdateKindSameFileNames) - updatedDiagnostics, err := session.handleGetSemanticDiagnostics(ctx, &GetDiagnosticsParams{ - Snapshot: updatedResponse.Snapshot, - Project: updatedResponse.Project.Id, - Files: []DocumentIdentifier{{FileName: fileName}}, + assert.NilError(t, utils.FS().WriteFile(fileName, `export const value = 2;`)) + dirty, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + Snapshot: created.Snapshot, + Changes: &CreateSnapshotParams{ + FileChanges: &APIFileChanges{Changed: []DocumentIdentifier{{FileName: fileName}}}, + }, }) assert.NilError(t, err) - assert.Equal(t, len(updatedDiagnostics), 0) + assert.Equal(t, dirty.Projects[0].Dirty, true) - _, err = session.handleRelease(ctx, &ReleaseParams{Snapshot: updatedResponse.Snapshot}) - assert.NilError(t, err) - baseDiagnostics, err := session.handleGetSemanticDiagnostics(ctx, &GetDiagnosticsParams{ - Snapshot: baseResponse.Snapshot, - Project: baseProject.Id, - Files: []DocumentIdentifier{{FileName: fileName}}, + ensured, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + Snapshot: dirty.Snapshot, + Changes: &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + EnsurePrograms: &EnsurePrograms{Projects: []ProjectID{projectID}}, + }, + }, }) assert.NilError(t, err) - assert.Equal(t, len(baseDiagnostics), 1) + assert.Equal(t, ensured.Projects[0].Dirty, false) } diff --git a/tsc/internal/api/session_requestfilesystem_test.go b/tsc/internal/api/session_requestfilesystem_test.go index d3f71f5938ffb..a4f624b29ad26 100644 --- a/tsc/internal/api/session_requestfilesystem_test.go +++ b/tsc/internal/api/session_requestfilesystem_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/microsoft/TypeScript/tsc/internal/api/requestfilesystem" + "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/project" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) -func TestUpdateSnapshotUsesFullFileSystem(t *testing.T) { +func TestCreateSnapshotUsesFullFileSystem(t *testing.T) { t.Parallel() projectSession, _ := projecttestutil.Setup(map[string]any{ @@ -25,8 +26,8 @@ func TestUpdateSnapshotUsesFullFileSystem(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - response, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}, + response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}}, FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindFull, Files: map[string]string{ @@ -59,12 +60,16 @@ func TestUpdateSnapshotUsesFullFileSystem(t *testing.T) { // Supplying a new filesystem replaces inherited snapshot disk caches even // when the caller does not redundantly list every file in FileChanges. response, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - FileSystem: &requestfilesystem.RequestFileSystem{ - Kind: requestfilesystem.KindFull, - Files: map[string]string{ - "/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["src/index.ts", "src/other.ts"] }`, - "/src/index.ts": `export const value = "updated";`, - "/src/other.ts": `export const other = true;`, + Snapshot: response.Snapshot, + Changes: &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{EnsurePrograms: &EnsurePrograms{All: true}}, + FileSystem: &requestfilesystem.RequestFileSystem{ + Kind: requestfilesystem.KindFull, + Files: map[string]string{ + "/tsconfig.json": `{ "compilerOptions": { "noLib": true }, "files": ["src/index.ts", "src/other.ts"] }`, + "/src/index.ts": `export const value = "updated";`, + "/src/other.ts": `export const other = true;`, + }, }, }, }) @@ -100,8 +105,8 @@ func TestCreateProgramRetainsFullFileSystem(t *testing.T) { defer session.Close() ctx := context.Background() - base, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: "/old.ts"}}, + base, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenFiles: []DocumentIdentifier{{FileName: "/old.ts"}}}, FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindFull, Files: map[string]string{ @@ -113,18 +118,20 @@ func TestCreateProgramRetainsFullFileSystem(t *testing.T) { assert.NilError(t, err) assert.Equal(t, len(base.Projects), 1) - created, err := session.handleCreateProgram(ctx, &CreateProgramParams{ - RootFiles: []DocumentIdentifier{{FileName: "/new.ts"}}, - OldProgram: &CreateProgramOldProgramParams{ - Snapshot: base.Snapshot, - Project: base.Projects[0].Id, - }, + created, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: base.Snapshot, + Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CreatePrograms: []*CreateSnapshotProgramParams{{ + RootFiles: []DocumentIdentifier{{FileName: "/new.ts"}}, + Options: CreateProgramOptions{CompilerOptions: core.CompilerOptions{NoLib: core.TSTrue}}, + }}, + }}, }) assert.NilError(t, err) snapshot, err := session.getSnapshotData(created.Snapshot) assert.NilError(t, err) - program, err := snapshot.getProgram(created.Project.Id) + program, err := snapshot.getProgram(ProjectID((*created.Operation.CreatedPrograms)[0])) assert.NilError(t, err) assert.Assert(t, program.GetSourceFile("/new.ts") != nil) } @@ -139,16 +146,16 @@ func TestSnapshotUpdateFullFileSystemIsTotal(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - base, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{}) + base, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{}) assert.NilError(t, err) replaced, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ Snapshot: base.Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ + Changes: &CreateSnapshotParams{FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindFull, Files: map[string]string{ "/memory.ts": "memory", }, - }, + }}, }) assert.NilError(t, err) @@ -171,8 +178,8 @@ func TestSnapshotUpdateCarriesHostFileSystemWithoutOverride(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - base, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}, + base, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}}, }) assert.NilError(t, err) baseSnapshot := session.snapshots[base.Snapshot].snapshot @@ -207,13 +214,13 @@ func TestSnapshotFileSystemLayersPreserveIncrementalState(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() ctx := context.Background() - params := &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: "/a/tsconfig.json"}, {FileName: "/b/tsconfig.json"}}, + params := &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenProjects: []DocumentIdentifier{{FileName: "/a/tsconfig.json"}, {FileName: "/b/tsconfig.json"}}}, } if baseKind != "host" { params.FileSystem = &requestfilesystem.RequestFileSystem{Kind: baseKind, Files: files} } - base, err := session.handleUpdateSnapshot(ctx, params) + base, err := session.handleCreateSnapshot(ctx, params) assert.NilError(t, err) baseSnapshot := session.snapshots[base.Snapshot].snapshot baseProgram := baseSnapshot.ProjectCollection.GetProjectByPath("/a/tsconfig.json").GetProgram() @@ -222,9 +229,12 @@ func TestSnapshotFileSystemLayersPreserveIncrementalState(t *testing.T) { unchanged, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ Snapshot: base.Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ - Kind: requestfilesystem.KindLayer, - Files: map[string]string{"/a/index.ts": files["/a/index.ts"]}, + Changes: &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{EnsurePrograms: &EnsurePrograms{All: true}}, + FileSystem: &requestfilesystem.RequestFileSystem{ + Kind: requestfilesystem.KindLayer, + Files: map[string]string{"/a/index.ts": files["/a/index.ts"]}, + }, }, }) assert.NilError(t, err) @@ -236,9 +246,12 @@ func TestSnapshotFileSystemLayersPreserveIncrementalState(t *testing.T) { const updatedText = `export const value = 2;` updated, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ Snapshot: unchanged.Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ - Kind: requestfilesystem.KindLayer, - Files: map[string]string{"/a/index.ts": updatedText}, + Changes: &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{EnsurePrograms: &EnsurePrograms{All: true}}, + FileSystem: &requestfilesystem.RequestFileSystem{ + Kind: requestfilesystem.KindLayer, + Files: map[string]string{"/a/index.ts": updatedText}, + }, }, }) assert.NilError(t, err) @@ -252,9 +265,12 @@ func TestSnapshotFileSystemLayersPreserveIncrementalState(t *testing.T) { removed, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ Snapshot: updated.Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ - Kind: requestfilesystem.KindLayer, - RemovedPaths: []string{"/a/removed"}, + Changes: &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{EnsurePrograms: &EnsurePrograms{All: true}}, + FileSystem: &requestfilesystem.RequestFileSystem{ + Kind: requestfilesystem.KindLayer, + RemovedPaths: []string{"/a/removed"}, + }, }, }) assert.NilError(t, err) @@ -270,7 +286,7 @@ func TestSnapshotFileSystemLayersPreserveIncrementalState(t *testing.T) { // A request without a base snapshot returns to the host, so the old // layer's changed contents and directory tombstones must not survive. - restored, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{}) + restored, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{SnapshotRequestChangesParams: params.SnapshotRequestChangesParams}) assert.NilError(t, err) restoredSnapshot := session.snapshots[restored.Snapshot].snapshot assert.Assert(t, !restoredSnapshot.HasFileSystemOverride()) @@ -292,13 +308,14 @@ func TestSnapshotFileSystemLayerWithoutBaseUpdatesHostState(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() ctx := context.Background() - _, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}, + _, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}}, }) assert.NilError(t, err) const updatedText = `export const value = 2;` - updated, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + updated, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}}, FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindLayer, Files: map[string]string{"/index.ts": updatedText}, @@ -308,7 +325,7 @@ func TestSnapshotFileSystemLayerWithoutBaseUpdatesHostState(t *testing.T) { snapshot := session.snapshots[updated.Snapshot].snapshot updatedProject := snapshot.ProjectCollection.GetProjectByPath("/tsconfig.json") assert.Equal(t, updatedProject.GetProgram().GetSourceFile("/index.ts").Text(), updatedText) - assert.Equal(t, updatedProject.ProgramUpdateKind, project.ProgramUpdateKindCloned) + assert.Equal(t, updatedProject.ProgramUpdateKind, project.ProgramUpdateKindNewFiles) } func TestEmitFromLayerOverFullFileSystemReturnsFileContents(t *testing.T) { @@ -320,8 +337,8 @@ func TestEmitFromLayerOverFullFileSystemReturnsFileContents(t *testing.T) { defer session.Close() ctx := context.Background() - base, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}, + base, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{OpenProjects: []DocumentIdentifier{{FileName: "/tsconfig.json"}}}, FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindFull, Files: map[string]string{ @@ -333,17 +350,17 @@ func TestEmitFromLayerOverFullFileSystemReturnsFileContents(t *testing.T) { assert.NilError(t, err) layered, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ Snapshot: base.Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ + Changes: &CreateSnapshotParams{FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindLayer, Files: map[string]string{}, - }, + }}, }) assert.NilError(t, err) - assert.Equal(t, len(layered.Projects), 1) + assert.Equal(t, len(layered.Projects), 0) emitted, err := session.handleEmit(ctx, &EmitParams{ Snapshot: layered.Snapshot, - Project: layered.Projects[0].Id, + Project: base.Projects[0].Id, }) assert.NilError(t, err) assert.DeepEqual(t, emitted.EmittedFiles, []string{"/out/src/main.js"}) @@ -353,7 +370,7 @@ func TestEmitFromLayerOverFullFileSystemReturnsFileContents(t *testing.T) { assert.NilError(t, err) emittedAfterRelease, err := session.handleEmit(ctx, &EmitParams{ Snapshot: layered.Snapshot, - Project: layered.Projects[0].Id, + Project: base.Projects[0].Id, }) assert.NilError(t, err) assert.DeepEqual(t, emittedAfterRelease.EmittedFiles, emitted.EmittedFiles) @@ -370,7 +387,7 @@ func TestReleaseSnapshotCompactsSoleLayeredFileSystem(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - base, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + base, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindFull, Files: map[string]string{ @@ -386,14 +403,14 @@ func TestReleaseSnapshotCompactsSoleLayeredFileSystem(t *testing.T) { layered, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ Snapshot: base.Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ + Changes: &CreateSnapshotParams{FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindLayer, Files: map[string]string{ "/changed.ts": "new", "/added.ts": "added", }, RemovedPaths: []string{"/removed.ts"}, - }, + }}, }) assert.NilError(t, err) layeredSnapshotData := session.snapshots[layered.Snapshot] @@ -430,7 +447,7 @@ func TestEagerSnapshotReleaseDoesNotRetainFileSystemHistory(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - response, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + response, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindFull, Files: map[string]string{ @@ -446,12 +463,12 @@ func TestEagerSnapshotReleaseDoesNotRetainFileSystemHistory(t *testing.T) { content += string(character) response, err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ Snapshot: oldSnapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ + Changes: &CreateSnapshotParams{FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindLayer, Files: map[string]string{ "/pkg/index.ts": content, }, - }, + }}, }) assert.NilError(t, err) _, err = session.handleRelease(context.Background(), &ReleaseParams{Snapshot: oldSnapshot}) @@ -478,9 +495,9 @@ func TestSnapshotReleaseCompactsChainedFileSystems(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - responses := make([]*UpdateSnapshotResponse, 4) + responses := make([]*CreateSnapshotResponse, 4) var err error - responses[0], err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + responses[0], err = session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindFull, Files: map[string]string{"/pkg/index.ts": "0"}, @@ -490,10 +507,10 @@ func TestSnapshotReleaseCompactsChainedFileSystems(t *testing.T) { for i := 1; i < len(responses); i++ { responses[i], err = session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ Snapshot: responses[i-1].Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ + Changes: &CreateSnapshotParams{FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindLayer, Files: map[string]string{"/pkg/index.ts": strconv.Itoa(i)}, - }, + }}, }) assert.NilError(t, err) } @@ -523,7 +540,7 @@ func TestTemporarySnapshotRetainsLayeredFileSystemHistory(t *testing.T) { session := NewLSPSession(projectSession, nil) defer session.Close() - base, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + base, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindFull, Files: map[string]string{"/pkg/index.ts": "base"}, @@ -532,10 +549,10 @@ func TestTemporarySnapshotRetainsLayeredFileSystemHistory(t *testing.T) { assert.NilError(t, err) layered, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ Snapshot: base.Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ + Changes: &CreateSnapshotParams{FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindLayer, Files: map[string]string{"/pkg/index.ts": "layered"}, - }, + }}, }) assert.NilError(t, err) layeredFileSystem := session.snapshots[layered.Snapshot].fileSystem @@ -571,16 +588,16 @@ func TestSnapshotReleaseCompactionSupportsConcurrentReaders(t *testing.T) { for index := range 1024 { files[fmt.Sprintf("/pkg/file%d.ts", index)] = strconv.Itoa(index) } - base, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ + base, err := session.handleCreateSnapshot(context.Background(), &CreateSnapshotParams{ FileSystem: &requestfilesystem.RequestFileSystem{Kind: requestfilesystem.KindFull, Files: files}, }) assert.NilError(t, err) layered, err := session.handleUpdateSnapshot(context.Background(), &UpdateSnapshotParams{ Snapshot: base.Snapshot, - FileSystem: &requestfilesystem.RequestFileSystem{ + Changes: &CreateSnapshotParams{FileSystem: &requestfilesystem.RequestFileSystem{ Kind: requestfilesystem.KindLayer, Files: map[string]string{"/pkg/file0.ts": "updated"}, - }, + }}, }) assert.NilError(t, err) fileSystem := session.snapshots[layered.Snapshot].fileSystem diff --git a/tsc/internal/api/session_temporary_test.go b/tsc/internal/api/session_temporary_test.go index c198c4c137ad9..6603f52aafa39 100644 --- a/tsc/internal/api/session_temporary_test.go +++ b/tsc/internal/api/session_temporary_test.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/bundled" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil" + "github.com/microsoft/TypeScript/tsc/internal/tspath" "gotest.tools/v3/assert" ) @@ -35,8 +36,10 @@ func TestUpdateTemporarySnapshot(t *testing.T) { ctx := context.Background() - baseResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + baseResp, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + }, }) assert.NilError(t, err) assert.Assert(t, len(baseResp.Projects) > 0, "expected at least one project") @@ -44,7 +47,6 @@ func TestUpdateTemporarySnapshot(t *testing.T) { // The base snapshot should be the session's latest snapshot. baseHandle := baseResp.Snapshot - assert.Equal(t, session.latestSnapshot, baseHandle) // Sanity: the original content type-checks cleanly. baseDiags, err := session.handleGetSemanticDiagnostics(ctx, &GetDiagnosticsParams{ @@ -66,7 +68,6 @@ func TestUpdateTemporarySnapshot(t *testing.T) { assert.Assert(t, tempResp.Snapshot != baseHandle, "temporary snapshot should have a distinct handle") // The temporary snapshot must NOT become the session's latest snapshot. - assert.Equal(t, session.latestSnapshot, baseHandle, "latest snapshot must be unchanged by a temporary update") // The temporary snapshot reflects the overridden content and reports the error. tempProjectID := tempResp.Projects[0].Id @@ -118,8 +119,10 @@ func TestUpdateTemporarySnapshotAddsUnopenedFile(t *testing.T) { defer session.Close() ctx := context.Background() - baseResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: existingFileName}}, + baseResp, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenFiles: []DocumentIdentifier{{FileName: existingFileName}}, + }, }) assert.NilError(t, err) assert.Equal(t, len(baseResp.Projects), 1) @@ -152,7 +155,7 @@ func TestUpdateTemporarySnapshotRejectsUnsupportedExtension(t *testing.T) { ctx := context.Background() const fileName = "/home/projects/p/src/temporary.custom" - baseResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{}) + baseResp, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{}) assert.NilError(t, err) _, err = session.handleUpdateTemporarySnapshot(ctx, &UpdateTemporarySnapshotParams{ Snapshot: baseResp.Snapshot, @@ -180,8 +183,10 @@ func TestUpdateTemporarySnapshotUsesClientSnapshotAsBase(t *testing.T) { defer session.Close() ctx := context.Background() - baseResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ - OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + baseResp, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + }, }) assert.NilError(t, err) @@ -200,3 +205,150 @@ func TestUpdateTemporarySnapshotUsesClientSnapshotAsBase(t *testing.T) { _, err = session.handleRelease(ctx, &ReleaseParams{Snapshot: tempResp.Snapshot}) assert.NilError(t, err) } + +func TestUpdateTemporarySnapshotOmitsUnchangedProjects(t *testing.T) { + t.Parallel() + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + const firstConfig = "/home/projects/first/tsconfig.json" + const firstFile = "/home/projects/first/index.ts" + const secondConfig = "/home/projects/second/tsconfig.json" + const secondFile = "/home/projects/second/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + firstConfig: `{}`, + firstFile: `export const first = 1;`, + secondConfig: `{}`, + secondFile: `export const second = 1;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + ctx := context.Background() + baseResp, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenProjects: []DocumentIdentifier{{FileName: firstConfig}, {FileName: secondConfig}}, + }, + }) + assert.NilError(t, err) + assert.Equal(t, len(baseResp.Projects), 2) + + tempResp, err := session.handleUpdateTemporarySnapshot(ctx, &UpdateTemporarySnapshotParams{ + Snapshot: baseResp.Snapshot, + File: DocumentIdentifier{FileName: firstFile}, + NewText: `export const first = 2;`, + }) + assert.NilError(t, err) + assert.Equal(t, len(tempResp.Projects), 1) + assert.Equal(t, tempResp.Projects[0].ConfigFileName, firstConfig) +} + +func TestUpdateSnapshotDerivesFromExplicitBase(t *testing.T) { + t.Parallel() + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + const firstConfig = "/home/projects/first/tsconfig.json" + const firstFile = "/home/projects/first/index.ts" + const secondConfig = "/home/projects/second/tsconfig.json" + projectSession, utils := projecttestutil.Setup(map[string]any{ + firstConfig: `{}`, + firstFile: `export const first = 1;`, + secondConfig: `{}`, + "/home/projects/second/index.ts": `export const second = 1;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + + ctx := context.Background() + baseResp, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{ + SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + OpenProjects: []DocumentIdentifier{{FileName: firstConfig}, {FileName: secondConfig}}, + }, + }) + assert.NilError(t, err) + assert.NilError(t, utils.FS().WriteFile(firstFile, `export const first = 2;`)) + + updated, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: baseResp.Snapshot, + Changes: &CreateSnapshotParams{ + FileChanges: &APIFileChanges{Changed: []DocumentIdentifier{{FileName: firstFile}}}, + }, + }) + assert.NilError(t, err) + assert.Equal(t, len(updated.Projects), 1) + assert.Equal(t, updated.Projects[0].ConfigFileName, firstConfig) + assert.Equal(t, len(baseResp.Projects), 2) +} + +func TestUpdateSnapshotProjectOpensAreIdempotent(t *testing.T) { + t.Parallel() + + const configFileName = "/home/projects/p/tsconfig.json" + projectSession, _ := projecttestutil.Setup(map[string]any{ + configFileName: `{}`, + "/home/projects/p/index.ts": `export const value = 1;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + ctx := context.Background() + open := SnapshotRequestChangesParams{OpenProjects: []DocumentIdentifier{{FileName: configFileName}}} + + created, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{SnapshotRequestChangesParams: open}) + assert.NilError(t, err) + reopened, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: created.Snapshot, + Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: open}, + }) + assert.NilError(t, err) + closed, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: reopened.Snapshot, + Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CloseProjects: []DocumentIdentifier{{FileName: configFileName}}, + }}, + }) + assert.NilError(t, err) + closedSnapshot, err := session.getSnapshotData(closed.Snapshot) + assert.NilError(t, err) + assert.Assert(t, closedSnapshot.snapshot.ProjectCollection.ConfiguredProject(tspath.Path(configFileName)) == nil) + assert.DeepEqual(t, closed.Changes.RemovedProjects, []ProjectID{ProjectID(configFileName)}) +} + +func TestUpdateSnapshotFileOpensAreIdempotent(t *testing.T) { + t.Parallel() + + const fileName = "/home/projects/p/index.ts" + projectSession, _ := projecttestutil.Setup(map[string]any{ + "/home/projects/p/tsconfig.json": `{}`, + fileName: `export const value = 1;`, + }) + defer projectSession.Close() + session := NewLSPSession(projectSession, nil) + defer session.Close() + ctx := context.Background() + open := SnapshotRequestChangesParams{OpenFiles: []DocumentIdentifier{{FileName: fileName}}} + + created, err := session.handleCreateSnapshot(ctx, &CreateSnapshotParams{SnapshotRequestChangesParams: open}) + assert.NilError(t, err) + reopened, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: created.Snapshot, + Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: open}, + }) + assert.NilError(t, err) + closed, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + Snapshot: reopened.Snapshot, + Changes: &CreateSnapshotParams{SnapshotRequestChangesParams: SnapshotRequestChangesParams{ + CloseFiles: []DocumentIdentifier{{FileName: fileName}}, + }}, + }) + assert.NilError(t, err) + closedSnapshot, err := session.getSnapshotData(closed.Snapshot) + assert.NilError(t, err) + assert.Equal(t, len(closedSnapshot.snapshot.ProjectCollection.Projects()), 0) + assert.DeepEqual(t, closed.Changes.RemovedProjects, []ProjectID{ProjectID("/home/projects/p/tsconfig.json")}) +} diff --git a/tsc/internal/lsp/server.go b/tsc/internal/lsp/server.go index f23a7fcadcd3c..8f2cdc27ccc3a 100644 --- a/tsc/internal/lsp/server.go +++ b/tsc/internal/lsp/server.go @@ -1458,7 +1458,7 @@ func (c *crossProjectOrchestrator) GetProjectsForFile(ctx context.Context, uri l func (c *crossProjectOrchestrator) GetProjectsLoadingProjectTree(ctx context.Context, requestedProjectTrees *collections.Set[tspath.Path]) iter.Seq[ls.Project] { return func(yield func(ls.Project) bool) { c.server.session.WithSnapshotLoadingProjectTree(ctx, requestedProjectTrees, func(snapshot *project.Snapshot) { - for _, p := range snapshot.ProjectCollection.Projects() { + for _, p := range snapshot.ProjectCollection.LanguageServiceProjects() { if !yield(p) { return } @@ -2171,12 +2171,12 @@ func (s *Server) handleWorkspaceSymbol(ctx context.Context, params *lsproto.Work if params.TextDocument != nil && s.session.Config().WorkspaceSymbolsScope == lsutil.WorkspaceSymbolsScopeCurrentProject { uri := params.TextDocument.Uri s.session.WithSnapshotForDocument(ctx, uri, func(snapshot *project.Snapshot) { - programs := core.Map(snapshot.GetProjectsContainingFile(uri), ls.Project.GetProgram) + programs := core.Map(snapshot.GetLanguageServiceProjectsContainingFile(uri), ls.Project.GetProgram) provideSymbols(snapshot, programs) }) } else { s.session.WithSnapshotLoadingProjectTree(ctx, nil, func(snapshot *project.Snapshot) { - programs := core.Map(snapshot.ProjectCollection.Projects(), (*project.Project).GetProgram) + programs := core.Map(snapshot.ProjectCollection.LanguageServiceProjects(), (*project.Project).GetProgram) provideSymbols(snapshot, programs) }) } diff --git a/tsc/internal/project/customconfigfilename_test.go b/tsc/internal/project/customconfigfilename_test.go index c13900b39e980..6b90f0c569ba4 100644 --- a/tsc/internal/project/customconfigfilename_test.go +++ b/tsc/internal/project/customconfigfilename_test.go @@ -211,7 +211,7 @@ func TestCustomConfigFileName(t *testing.T) { // Without any config, the file should be in the inferred project only. snapshot := session.Snapshot() assert.Equal(t, snapshot.GetDefaultProject(uriLocal).Name(), "/dev/null/inferred") - projects := snapshot.GetProjectsContainingFile(uriLocal) + projects := snapshot.GetLanguageServiceProjectsContainingFile(uriLocal) assert.Equal(t, len(projects), 1, "expected file to be in exactly 1 project before config change, got %d", len(projects)) // Now set custom config to pick up tsconfig.all.json @@ -225,7 +225,7 @@ func TestCustomConfigFileName(t *testing.T) { // File should now be in the configured project only, not duplicated in inferred. snapshot = session.Snapshot() assert.Equal(t, snapshot.GetDefaultProject(uriLocal).Name(), "/src/tsconfig.all.json") - projects = snapshot.GetProjectsContainingFile(uriLocal) + projects = snapshot.GetLanguageServiceProjectsContainingFile(uriLocal) assert.Equal(t, len(projects), 1, "expected file to be in exactly 1 project after config change, got %d", len(projects)) }) } diff --git a/tsc/internal/project/project.go b/tsc/internal/project/project.go index 80cacf0b7b834..354e778da94be 100644 --- a/tsc/internal/project/project.go +++ b/tsc/internal/project/project.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "slices" + "strconv" "strings" "sync" @@ -21,10 +22,24 @@ import ( ) const ( - inferredProjectName = "/dev/null/inferred" // lowercase so toPath is a no-op regardless of settings - hr = "-----------------------------------------------" + inferredProjectName = "/dev/null/inferred" // lowercase so toPath is a no-op regardless of settings + syntheticProjectPrefix = "/dev/null/synthetic/" + hr = "-----------------------------------------------" ) +func syntheticProjectName(id int) string { + return fmt.Sprintf("%s%d", syntheticProjectPrefix, id) +} + +func SyntheticProgramID(path tspath.Path) (int, bool) { + value, ok := strings.CutPrefix(string(path), syntheticProjectPrefix) + if !ok { + return 0, false + } + id, err := strconv.Atoi(value) + return id, err == nil && id > 0 +} + //go:generate go tool golang.org/x/tools/cmd/stringer -type=Kind -trimprefix=Kind -output=project_stringer_generated.go //go:generate npx dprint fmt project_stringer_generated.go @@ -33,6 +48,7 @@ type Kind int const ( KindInferred Kind = iota KindConfigured + KindSynthetic ) type ProgramUpdateKind int @@ -139,6 +155,30 @@ func NewInferredProject( return p } +func newSyntheticProject( + name string, + currentDirectory string, + compilerOptions *core.CompilerOptions, + rootFileNames []string, + projectReferences []*core.ProjectReference, + contentMappers []*contentmapper.Mapper, + builder *ProjectCollectionBuilder, + logger *logging.LogTree, +) *Project { + project := NewProject(name, KindSynthetic, currentDirectory, builder, logger) + project.CommandLine = newInferredProjectCommandLine( + compilerOptions, + rootFileNames, + projectReferences, + contentMappers, + tspath.ComparePathsOptions{ + UseCaseSensitiveFileNames: builder.fs.fs.UseCaseSensitiveFileNames(), + CurrentDirectory: currentDirectory, + }, + ) + return project +} + func newInferredProjectCommandLine( compilerOptions *core.CompilerOptions, rootFileNames []string, @@ -151,24 +191,6 @@ func newInferredProjectCommandLine( return commandLine } -// newInferredProjectFromProject creates an isolated synthetic project seeded -// from an existing project's compiler state. -func newInferredProjectFromProject( - project *Project, - builder *ProjectCollectionBuilder, - logger *logging.LogTree, -) *Project { - inferred := NewProject(inferredProjectName, KindInferred, project.currentDirectory, builder, logger) - inferred.CommandLine = project.Program.CommandLine() - inferred.Program = project.Program - inferred.ProgramLastUpdate = project.ProgramLastUpdate - inferred.host = project.host - inferred.checkerPool = project.checkerPool - inferred.contentMapperWatchedFiles = project.contentMapperWatchedFiles - inferred.dirty = false - return inferred -} - func NewProject( configFileName string, kind Kind, @@ -261,6 +283,10 @@ func (p *Project) GetProgram() *compiler.Program { return p.Program } +func (p *Project) IsDirty() bool { + return p.dirty +} + // GetProjectDiagnostics returns program diagnostics combined with any global // diagnostics discovered during checking. These are the diagnostics reported on // the tsconfig.json file. @@ -505,8 +531,8 @@ func (p *Project) print(writeFileNames bool, writeFileExplanation bool, builder // GetTypeAcquisition returns the type acquisition settings for this project. func (p *Project) GetTypeAcquisition() *core.TypeAcquisition { - if p.Kind == KindInferred { - // For inferred projects, use default settings + if p.Kind == KindInferred || p.Kind == KindSynthetic { + // For inferred and synthetic projects, use default settings. return &core.TypeAcquisition{ Enable: core.TSTrue, Include: nil, diff --git a/tsc/internal/project/project_stringer_generated.go b/tsc/internal/project/project_stringer_generated.go index a0a58f43d2619..f3711d1ae6207 100644 --- a/tsc/internal/project/project_stringer_generated.go +++ b/tsc/internal/project/project_stringer_generated.go @@ -10,11 +10,12 @@ func _() { var x [1]struct{} _ = x[KindInferred-0] _ = x[KindConfigured-1] + _ = x[KindSynthetic-2] } -const _Kind_name = "InferredConfigured" +const _Kind_name = "InferredConfiguredSynthetic" -var _Kind_index = [...]uint8{0, 8, 18} +var _Kind_index = [...]uint8{0, 8, 18, 27} func (i Kind) String() string { idx := int(i) - 0 diff --git a/tsc/internal/project/projectcollection.go b/tsc/internal/project/projectcollection.go index 1bd2f8148a466..b0242b1f8430c 100644 --- a/tsc/internal/project/projectcollection.go +++ b/tsc/internal/project/projectcollection.go @@ -24,6 +24,8 @@ type ProjectCollection struct { // configuredProjects is the set of loaded projects associated with a tsconfig // file, keyed by the config file path. configuredProjects map[tspath.Path]*Project + // syntheticProjects contains synthetic projects created explicitly through the API. + syntheticProjects map[tspath.Path]*Project // openFiles is the set of open file paths associated with the snapshot that owns // this project collection. openFiles collections.Set[tspath.Path] @@ -43,8 +45,7 @@ type ProjectCollection struct { // other, and it is carried across snapshots so API-opened resources stay loaded. type APIState struct { // openProjects is the ref-counted set of projects to keep open for API - // clients, keyed by config file path. The value is the number of outstanding - // API opens. + // clients, keyed by config file path. openProjects map[tspath.Path]int // openFiles is the ref-counted set of files to keep open for API clients, // keyed by file path. Files with no configured project are loaded into the @@ -79,6 +80,9 @@ func (c *ProjectCollection) GetProjectByPath(projectPath tspath.Path) *Project { if project, ok := c.configuredProjects[projectPath]; ok { return project } + if project, ok := c.syntheticProjects[projectPath]; ok { + return project + } if projectPath == inferredProjectName { return c.inferredProject @@ -103,29 +107,56 @@ func (c *ProjectCollection) fillConfiguredProjects(projects *[]*Project) { }) } +// SyntheticProjects returns all synthetic projects in a stable order. +func (c *ProjectCollection) SyntheticProjects() []*Project { + projects := make([]*Project, 0, len(c.syntheticProjects)) + for _, project := range c.syntheticProjects { + projects = append(projects, project) + } + slices.SortFunc(projects, func(a, b *Project) int { + return cmp.Compare(a.Name(), b.Name()) + }) + return projects +} + // ProjectsByPath returns an ordered map of configured projects keyed by their config file path, -// plus the inferred project, if it exists, with the key `inferredProjectName`. +// followed by synthetic projects and the inferred project, if it exists. func (c *ProjectCollection) ProjectsByPath() *collections.OrderedMap[tspath.Path, *Project] { projects := collections.NewOrderedMapWithSizeHint[tspath.Path, *Project]( - len(c.configuredProjects) + core.IfElse(c.inferredProject != nil, 1, 0), + len(c.configuredProjects) + len(c.syntheticProjects) + core.IfElse(c.inferredProject != nil, 1, 0), ) for _, project := range c.ConfiguredProjects() { projects.Set(project.configFilePath, project) } + for _, project := range c.SyntheticProjects() { + projects.Set(project.configFilePath, project) + } if c.inferredProject != nil { projects.Set(inferredProjectName, c.inferredProject) } return projects } -// Projects returns all projects, including the inferred project if it exists, in a stable order. +// Projects returns all configured, synthetic, and inferred projects in a stable order. func (c *ProjectCollection) Projects() []*Project { - if c.inferredProject == nil { - return c.ConfiguredProjects() + projects := make([]*Project, 0, len(c.configuredProjects)+len(c.syntheticProjects)+core.IfElse(c.inferredProject != nil, 1, 0)) + c.fillConfiguredProjects(&projects) + projects = append(projects, c.SyntheticProjects()...) + if c.inferredProject != nil { + projects = append(projects, c.inferredProject) } - projects := make([]*Project, 0, len(c.configuredProjects)+1) + return projects +} + +// LanguageServiceProjects returns configured and inferred projects in stable order. +// Synthetic projects are accessed explicitly through the API and do not participate +// in cross-project language service operations. +func (c *ProjectCollection) LanguageServiceProjects() []*Project { + projects := make([]*Project, 0, len(c.configuredProjects)+core.IfElse(c.inferredProject != nil, 1, 0)) c.fillConfiguredProjects(&projects) - projects = append(projects, c.inferredProject) + if c.inferredProject != nil { + projects = append(projects, c.inferredProject) + } return projects } @@ -133,7 +164,9 @@ func (c *ProjectCollection) InferredProject() *Project { return c.inferredProject } -func (c *ProjectCollection) GetProjectsContainingFile(path tspath.Path) []ls.Project { +// GetLanguageServiceProjectsContainingFile does not consider synthetic projects +// (ones created by API via createProgram) +func (c *ProjectCollection) GetLanguageServiceProjectsContainingFile(path tspath.Path) []ls.Project { var projects []ls.Project for _, project := range c.ConfiguredProjects() { if project.containsFile(path) { @@ -300,6 +333,7 @@ func (c *ProjectCollection) clone() *ProjectCollection { toPath: c.toPath, configFileRegistry: c.configFileRegistry, configuredProjects: c.configuredProjects, + syntheticProjects: c.syntheticProjects, openFiles: c.openFiles, inferredProject: c.inferredProject, fileDefaultProjects: c.fileDefaultProjects, diff --git a/tsc/internal/project/projectcollectionbuilder.go b/tsc/internal/project/projectcollectionbuilder.go index 347b69caf7a1a..884c0401a85e9 100644 --- a/tsc/internal/project/projectcollectionbuilder.go +++ b/tsc/internal/project/projectcollectionbuilder.go @@ -6,6 +6,7 @@ import ( "maps" "reflect" "slices" + "sync" "time" "github.com/microsoft/TypeScript/tsc/internal/ast" @@ -56,7 +57,9 @@ type ProjectCollectionBuilder struct { fileDefaultProjects map[tspath.Path]tspath.Path configuredProjects *dirty.SyncMap[tspath.Path, *Project] + syntheticProjects *dirty.SyncMap[tspath.Path, *Project] inferredProject *dirty.Box[*Project] + createdPrograms []*Project apiState APIState } @@ -95,6 +98,7 @@ func newProjectCollectionBuilder( configFileRegistryBuilder: newConfigFileRegistryBuilder(lsproto.GetClientCapabilities(ctx).Workspace.DidChangeWatchedFiles.RelativePatternSupport, fs, oldConfigFileRegistry, extendedConfigCache, newSnapshotID, sessionOptions, customConfigFileName, nil), newSnapshotID: newSnapshotID, configuredProjects: dirty.NewSyncMap(oldProjectCollection.configuredProjects), + syntheticProjects: dirty.NewSyncMap(oldProjectCollection.syntheticProjects), inferredProject: dirty.NewBox(oldProjectCollection.inferredProject), apiState: oldAPIState.clone(), client: client, @@ -115,6 +119,10 @@ func (b *ProjectCollectionBuilder) Finalize(logger *logging.LogTree) (*ProjectCo ensureCloned() newProjectCollection.configuredProjects = configuredProjects } + if syntheticProjects, syntheticProjectsChanged := b.syntheticProjects.Finalize(); syntheticProjectsChanged { + ensureCloned() + newProjectCollection.syntheticProjects = syntheticProjects + } if b.openFilesChanged { ensureCloned() @@ -151,6 +159,12 @@ func (b *ProjectCollectionBuilder) forEachProject(fn func(entry dirty.Value[*Pro keepGoing = fn(entry) return keepGoing }) + if keepGoing { + b.syntheticProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { + keepGoing = fn(entry) + return keepGoing + }) + } if !keepGoing { return } @@ -163,8 +177,6 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque var projectsToClose map[tspath.Path]struct{} if apiRequest.CloseProjects != nil { for projectPath := range apiRequest.CloseProjects.Keys() { - // Ref-counted close: only actually close the project once the last - // API client that opened it releases it. if count := b.apiState.openProjects[projectPath]; count > 1 { b.apiState.openProjects[projectPath] = count - 1 } else if count == 1 { @@ -187,6 +199,7 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque b.apiState.openProjects[configPath]++ // A project re-opened in the same request shouldn't be closed. delete(projectsToClose, configPath) + b.updateProgram(entry, logger) } else { return fmt.Errorf("project not found for open: %s", configFileName) } @@ -195,7 +208,6 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque if apiRequest.CloseFiles != nil { for path := range apiRequest.CloseFiles.Keys() { - // Ref-counted close mirroring projects above. if entry, ok := b.apiState.openFiles[path]; ok { if entry.refCount > 1 { entry.refCount-- @@ -221,14 +233,6 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque } } - for configPath := range b.apiState.openProjects { - if entry, ok := b.configuredProjects.Load(configPath); ok { - b.updateProgram(entry, logger) - } else { - return fmt.Errorf("project not found for update: %s", configPath) - } - } - for _, overlay := range b.fs.overlays { if entry := b.findDefaultConfiguredProject(overlay.FileName(), b.toPath(overlay.FileName())); entry != nil { delete(projectsToClose, entry.Value().configFilePath) @@ -237,36 +241,92 @@ func (b *ProjectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque for projectPath := range projectsToClose { if entry, ok := b.configuredProjects.Load(projectPath); ok { - b.deleteConfiguredProject(entry, logger) + b.deleteProject(entry, logger) } } - // Ensure each API-opened file is placed like LSP's textDocument/didOpen: search - // up ancestor directories for a configured project that contains it, and only - // fall back to the inferred project if none is found. This also keeps already - // loaded configured projects up to date. Then run the same cleanup the LSP open - // path uses, so configured projects auto-loaded for files that are no longer open - // are torn down instead of leaking. - if apiRequest.OpenFiles != nil || apiRequest.CloseFiles != nil { + // Place newly API-opened files like LSP's textDocument/didOpen, ensuring only + // their target projects. Existing API-opened files are retained by cleanup below + // without implicitly updating their programs. + if apiRequest.OpenFiles != nil { var retain collections.Set[tspath.Path] - for path, file := range b.apiState.openFiles { + ensureInferredProject := false + for uri := range apiRequest.OpenFiles.Keys() { + fileName := uri.FileName() + path := b.toPath(fileName) if b.fs.isOpenFile(path) { - // Already an LSP overlay; its project membership is handled by the - // overlay pass in cleanupConfiguredProjects. continue } - result := b.ensureConfiguredProjectAndAncestorsForFile(file.fileName, path, logger) + result := b.ensureConfiguredProjectAndAncestorsForFile(fileName, path, logger) retain.Union(&result.retain) + if result.project == nil { + ensureInferredProject = true + } } b.cleanupConfiguredProjects(&retain, logger) + if ensureInferredProject && b.inferredProject.Value() != nil { + b.updateProgram(b.inferredProject, logger) + } + } else if apiRequest.CloseFiles != nil { + b.cleanupConfiguredProjects(nil, logger) + } + for programID := range apiRequest.RemovePrograms.Keys() { + projectPath := b.toPath(syntheticProjectName(programID)) + project, ok := b.syntheticProjects.Load(projectPath) + if !ok { + return fmt.Errorf("synthetic program not found for removal: %d", programID) + } + b.deleteProject(project, logger) + } + createdPrograms := make([]*Project, len(apiRequest.CreatePrograms)) + entries := make([]*dirty.SyncMapEntry[tspath.Path, *Project], len(apiRequest.CreatePrograms)) + for i, request := range apiRequest.CreatePrograms { + entry := b.updateOrCreateSyntheticProject( + b.nextSyntheticProjectName(), + slices.Clone(request.RootFileNames), + request.CompilerOptions, + request.ProjectReferences, + request.ConfigFileParsingDiagnostics, + b.inferredContentMappers, + logger, + ) + entries[i] = entry } - if b.inferredProject.Value() != nil { - b.updateProgram(b.inferredProject, logger) + var wg sync.WaitGroup + for i, entry := range entries { + wg.Go(func() { + if entry.Value().dirty { + b.updateProgram(entry, logger) + } + createdPrograms[i] = entry.Value() + }) + } + wg.Wait() + b.createdPrograms = createdPrograms + for uri := range apiRequest.EnsureFiles.Keys() { + b.DidRequestFile(uri, false /*configuredProjectsOnly*/, logger) + } + for projectID := range apiRequest.EnsurePrograms.Keys() { + b.DidRequestProject(projectID, logger) + } + if apiRequest.EnsureAllPrograms { + b.forEachProject(func(entry dirty.Value[*Project]) bool { + b.updateProgram(entry, logger) + return true + }) } - return nil } +func (b *ProjectCollectionBuilder) nextSyntheticProjectName() string { + for id := 1; ; id++ { + name := syntheticProjectName(id) + if _, ok := b.syntheticProjects.Load(b.toPath(name)); !ok { + return name + } + } +} + func (b *ProjectCollectionBuilder) DidChangeFiles(summary FileChangeSummary, logger *logging.LogTree) { b.openFilesChanged = b.openFilesChanged || summary.Opened != "" || summary.Closed.Len() > 0 @@ -442,7 +502,7 @@ func (b *ProjectCollectionBuilder) cleanupConfiguredProjects(retain *collections continue } if p, ok := b.configuredProjects.Load(projectPath); ok { - b.deleteConfiguredProject(p, logger) + b.deleteProject(p, logger) } } b.updateInferredProjectRoots(inferredProjectFiles, logger) @@ -453,7 +513,7 @@ func (b *ProjectCollectionBuilder) cleanupConfiguredProjects(retain *collections func (b *ProjectCollectionBuilder) cleanupAllConfiguredProjects(logger *logging.LogTree) { b.configuredProjects.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *Project]) bool { if p, ok := b.configuredProjects.Load(entry.Key()); ok { - b.deleteConfiguredProject(p, logger) + b.deleteProject(p, logger) } return true }) @@ -590,7 +650,9 @@ func (b *ProjectCollectionBuilder) DidRequestProject(projectId tspath.Path, logg b.updateProgram(b.inferredProject, logger) } } else { - if entry, ok := b.configuredProjects.Load(projectId); ok { + if entry, ok := b.syntheticProjects.Load(projectId); ok { + b.updateProgram(entry, logger) + } else if entry, ok := b.configuredProjects.Load(projectId); ok { b.updateProgram(entry, logger) } } @@ -767,7 +829,11 @@ func (b *ProjectCollectionBuilder) markProjectsAffectedByConfigChanges( if projectPath == inferredProjectName { project = b.inferredProject } else { - project, _ = b.configuredProjects.Load(projectPath) + if syntheticProject, ok := b.syntheticProjects.Load(projectPath); ok { + project = syntheticProject + } else { + project, _ = b.configuredProjects.Load(projectPath) + } } if project == nil || project.Value() == nil { panic(fmt.Sprintf("project %s affected by config change not found", projectPath)) @@ -1131,17 +1197,49 @@ func (b *ProjectCollectionBuilder) updateInferredProjectRoots(rootFileNames []st return b.updateInferredProject(rootFileNames, b.compilerOptionsForInferredProjects, projectReferences, configFileParsingDiagnostics, b.inferredContentMappers, logger) } -// seedInferredProjectForProgram copies the specified project into the synthetic inferred project used by createProgram. -func (b *ProjectCollectionBuilder) seedInferredProjectForProgram(project *Project, logger *logging.LogTree) { - if project == nil || project.Program == nil { - return +func (b *ProjectCollectionBuilder) updateOrCreateSyntheticProject( + name string, + rootFileNames []string, + compilerOptions *core.CompilerOptions, + projectReferences []*core.ProjectReference, + configFileParsingDiagnostics []*ast.Diagnostic, + contentMappers []*contentmapper.Mapper, + logger *logging.LogTree, +) *dirty.SyncMapEntry[tspath.Path, *Project] { + projectPath := b.toPath(name) + project, loaded := b.syntheticProjects.Load(projectPath) + if !loaded { + syntheticProject := newSyntheticProject(name, b.sessionOptions.CurrentDirectory, compilerOptions, rootFileNames, projectReferences, contentMappers, b, logger) + syntheticProject.CommandLine.Errors = configFileParsingDiagnostics + project, _ = b.syntheticProjects.LoadOrStore(projectPath, syntheticProject) + return project } - inferredProject := newInferredProjectFromProject(project, b, logger) - project.Program.RangeResolvedProjectReference(func(referencePath tspath.Path, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { - b.configFileRegistryBuilder.retainConfigForProject(referencePath, inferredProject.configFilePath) - return true + + currentProject := project.Value() + if compilerOptions == nil { + compilerOptions = currentProject.CommandLine.CompilerOptions() + } + newCommandLine := newInferredProjectCommandLine(compilerOptions, rootFileNames, projectReferences, contentMappers, tspath.ComparePathsOptions{ + UseCaseSensitiveFileNames: b.fs.fs.UseCaseSensitiveFileNames(), + CurrentDirectory: currentProject.currentDirectory, }) - b.inferredProject.Set(inferredProject) + newCommandLine.Errors = configFileParsingDiagnostics + project.ChangeIf( + func(p *Project) bool { + return !slices.Equal(p.CommandLine.FileNames(), newCommandLine.FileNames()) || + !reflect.DeepEqual(p.CommandLine.CompilerOptions(), compilerOptions) || + !projectReferencesEqual(p.CommandLine.ProjectReferences(), projectReferences) || + !reflect.DeepEqual(p.CommandLine.Errors, configFileParsingDiagnostics) || + !slices.Equal(p.CommandLine.ContentMappers(), newCommandLine.ContentMappers()) + }, + func(p *Project) { + if logger != nil { + logger.Log(fmt.Sprintf("Updating synthetic project config with %d root files", len(rootFileNames))) + } + p.SetCommandLine(newCommandLine) + }, + ) + return project } // updateInferredProject preserves the current command line when roots/options are unchanged. @@ -1154,20 +1252,31 @@ func (b *ProjectCollectionBuilder) updateInferredProject( logger *logging.LogTree, ) bool { if len(rootFileNames) == 0 { - if b.inferredProject.Value() != nil { - if logger != nil { - logger.Log("Deleting inferred project") - } - b.inferredProject.Delete() - return true - } - return false + return b.deleteInferredProject(logger) } rootFileNames = slices.Clone(rootFileNames) slices.Sort(rootFileNames) return b.updateOrCreateInferredProject(rootFileNames, compilerOptions, projectReferences, configFileParsingDiagnostics, contentMappers, logger) } +func (b *ProjectCollectionBuilder) deleteInferredProject(logger *logging.LogTree) bool { + project := b.inferredProject.Value() + if project == nil { + return false + } + if logger != nil { + logger.Log("Deleting inferred project") + } + if project.Program != nil { + project.Program.RangeResolvedProjectReference(func(referencePath tspath.Path, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + b.configFileRegistryBuilder.releaseConfigForProject(referencePath, project.configFilePath) + return true + }) + } + b.inferredProject.Delete() + return true +} + // updateOrCreateInferredProject always retains an inferred project, including when rootFileNames is empty. // The caller transfers ownership of rootFileNames. func (b *ProjectCollectionBuilder) updateOrCreateInferredProject( @@ -1278,7 +1387,7 @@ func (b *ProjectCollectionBuilder) updateProgram(entry dirty.Value[*Project], lo b.client.ProgressStart(diagnostics.Project_0, displayName) } if deleteProject { - b.deleteConfiguredProject(entry, logger) + b.deleteProject(entry, logger) } if updateProgram { entry.Locked(func(entry dirty.Value[*Project]) { @@ -1395,18 +1504,21 @@ func (b *ProjectCollectionBuilder) markFilesChanged(entry dirty.Value[*Project], ) } -func (b *ProjectCollectionBuilder) deleteConfiguredProject(project dirty.Value[*Project], logger *logging.LogTree) { - projectPath := project.Value().configFilePath +func (b *ProjectCollectionBuilder) deleteProject(project dirty.Value[*Project], logger *logging.LogTree) { + value := project.Value() + projectPath := value.configFilePath if logger != nil { - logger.Log("Deleting configured project: " + project.Value().configFileName) + logger.Logf("Deleting %s project: %s", value.Kind.String(), value.Name()) } - if program := project.Value().Program; program != nil { - program.RangeResolvedProjectReference(func(referencePath tspath.Path, config *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { + if value.Program != nil { + value.Program.RangeResolvedProjectReference(func(referencePath tspath.Path, _ *tsoptions.ParsedCommandLine, _ *tsoptions.ParsedCommandLine, _ int) bool { b.configFileRegistryBuilder.releaseConfigForProject(referencePath, projectPath) return true }) } - b.configFileRegistryBuilder.releaseConfigForProject(projectPath, projectPath) + if value.Kind == KindConfigured { + b.configFileRegistryBuilder.releaseConfigForProject(projectPath, projectPath) + } project.Delete() } diff --git a/tsc/internal/project/refcountcache_test.go b/tsc/internal/project/refcountcache_test.go index 2edd1d4d3f970..5fea9e32a76c1 100644 --- a/tsc/internal/project/refcountcache_test.go +++ b/tsc/internal/project/refcountcache_test.go @@ -468,7 +468,7 @@ func TestRefCountingCaches(t *testing.T) { ResourceRequest: ResourceRequest{ Documents: []lsproto.DocumentUri{uri}, }, - }, baseSnapshot.fs.overlays, nil) + }, baseSnapshot.fs.overlays, nil, nil) project := clone.GetDefaultProject(uri) assert.Assert(t, project != nil) @@ -521,21 +521,23 @@ func TestRefCountingCaches(t *testing.T) { appProject := baseSnapshot.ProjectCollection.GetProjectByPath(baseSnapshot.toPath(appConfigPath)) assert.Assert(t, appProject != nil) - programSnapshot := session.CloneSnapshotForProgram( + createRequest := &APISnapshotRequest{CreatePrograms: []*APICreateProgramRequest{{ + RootFileNames: appProject.CommandLine.FileNames(), + CompilerOptions: appProject.CommandLine.CompilerOptions(), + ProjectReferences: appProject.CommandLine.ProjectReferences(), + ConfigFileParsingDiagnostics: appProject.CommandLine.Errors, + }}} + programSnapshot, err := session.CloneSnapshot( ctx, baseSnapshot, - nil, - appProject.CommandLine.FileNames(), - appProject.CommandLine.CompilerOptions(), - appProject.CommandLine.ProjectReferences(), - appProject.CommandLine.Errors, - appProject, FileChangeSummary{}, + createRequest, ) + assert.NilError(t, err) defer programSnapshot.Deref() - programProject := programSnapshot.ProjectCollection.InferredProject() + programProject := programSnapshot.CreatedPrograms()[0] assert.Assert(t, programProject != nil) - assert.Assert(t, programProject.Program == appProject.Program) + assert.Assert(t, programProject.Program != appProject.Program) extendedConfigEntry, ok := session.extendedConfigCache.entries.Load(tspath.Path(libBaseConfigPath)) assert.Assert(t, ok) @@ -551,19 +553,16 @@ func TestRefCountingCaches(t *testing.T) { assert.NilError(t, session.fs.fs.WriteFile(libBaseConfigPath, `{"compilerOptions":{"composite":true,"noLib":true,"strict":true}}`)) var fileChanges FileChangeSummary fileChanges.Changed.Add(lsproto.DocumentUri("file://" + libBaseConfigPath)) - updatedProgramSnapshot := session.CloneSnapshotForProgram( + updateRequest := &APISnapshotRequest{EnsurePrograms: collections.NewSetFromItems(programProject.ID())} + updatedProgramSnapshot, err := session.CloneSnapshot( ctx, programSnapshot, - nil, - programProject.CommandLine.FileNames(), - programProject.CommandLine.CompilerOptions(), - programProject.CommandLine.ProjectReferences(), - programProject.CommandLine.Errors, - programProject, fileChanges, + updateRequest, ) + assert.NilError(t, err) defer updatedProgramSnapshot.Deref() - updatedProgramProject := updatedProgramSnapshot.ProjectCollection.InferredProject() + updatedProgramProject := updatedProgramSnapshot.ProjectCollection.GetProjectByPath(programProject.ID()) assert.Assert(t, updatedProgramProject != nil) assert.Assert(t, updatedProgramProject.Program != programProject.Program) updatedReferences := updatedProgramProject.Program.GetResolvedProjectReferences() diff --git a/tsc/internal/project/session.go b/tsc/internal/project/session.go index 545287e54af33..86324e7551da3 100644 --- a/tsc/internal/project/session.go +++ b/tsc/internal/project/session.go @@ -1129,7 +1129,7 @@ func (s *Session) GetLanguageServiceAndProjectsForFile(ctx context.Context, uri return nil, nil, nil, err } // !!! TODO: sheetal: Get other projects that contain the file with symlink - allProjects := snapshot.GetProjectsContainingFile(uri) + allProjects := snapshot.GetLanguageServiceProjectsContainingFile(uri) return project, defaultLs, allProjects, nil } @@ -1141,7 +1141,7 @@ func (s *Session) GetProjectsForFile(ctx context.Context, uri lsproto.DocumentUr ) // !!! TODO: sheetal: Get other projects that contain the file with symlink - allProjects := snapshot.GetProjectsContainingFile(uri) + allProjects := snapshot.GetLanguageServiceProjectsContainingFile(uri) return allProjects, nil } @@ -1165,7 +1165,7 @@ func (s *Session) GetLanguageServicesForDocumentsLoadingProjectTree(ctx context. activeFile = uris[0].FileName() } - projects := snapshot.ProjectCollection.Projects() + projects := snapshot.ProjectCollection.LanguageServiceProjects() services := make([]*ls.LanguageService, 0, len(projects)) for _, project := range projects { program := project.GetProgram() @@ -1359,8 +1359,7 @@ func (s *Session) updateSnapshot(ctx context.Context, overlays map[tspath.Path]* if !locale.HasLocale(ctx) { ctx = s.WithCurrentLocale(ctx) } - change.client = s.client - newSnapshot := oldSnapshot.Clone(ctx, change, overlays, s.logger) + newSnapshot := oldSnapshot.Clone(ctx, change, overlays, s.logger, s.client) s.snapshot = newSnapshot if callerRef { newSnapshot.ref() @@ -2083,13 +2082,12 @@ func (s *Session) warmAutoImportCache(ctx context.Context, change SnapshotChange warmChange := SnapshotChange{ reason: UpdateReasonRequestedLanguageServiceWithAutoImports, - client: s.client, ResourceRequest: ResourceRequest{ Documents: []lsproto.DocumentUri{changedFile}, AutoImports: changedFile, }, } - clonedSnapshot := newSnapshot.Clone(warmCtx, warmChange, newSnapshot.fs.overlays, s.logger) + clonedSnapshot := newSnapshot.Clone(warmCtx, warmChange, newSnapshot.fs.overlays, s.logger, s.client) // If cancelled during clone, discard the incomplete result. if warmCtx.Err() != nil { diff --git a/tsc/internal/project/snapshot.go b/tsc/internal/project/snapshot.go index ad4426ea3ddc0..f37ee4be2c3e4 100644 --- a/tsc/internal/project/snapshot.go +++ b/tsc/internal/project/snapshot.go @@ -54,6 +54,8 @@ type Snapshot struct { // fileSystemOverride indicates that this snapshot was built from a filesystem // supplied by an API update rather than the session host filesystem. fileSystemOverride bool + + createdPrograms []*Project } func (s *Snapshot) contentMapperWatchState() ([]string, *collections.Set[tspath.Path]) { @@ -104,148 +106,12 @@ func (host *SnapshotHost) newSnapshot( return s } -// cloneForProgram clones a snapshot and creates a single synthetic inferred -// project representing createProgram input. -func (s *Snapshot) cloneForProgram( - ctx context.Context, - fileSystem vfs.FS, - rootFileNames []string, - compilerOptions *core.CompilerOptions, - projectReferences []*core.ProjectReference, - configFileParsingDiagnostics []*ast.Diagnostic, - oldProject *Project, - fileChanges FileChangeSummary, - sessionLogger logging.Logger, -) *Snapshot { - store := s.host - var logger *logging.LogTree - - if store.options.LoggingEnabled && sessionLogger != nil { - defer func() { - if r := recover(); r != nil { - sessionLogger.Log(logger.String()) - panic(r) - } - }() - logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d for program", s.id)) - } - - start := time.Now() - if fileSystem == nil { - fileSystem = store.fs - } - fs := newSnapshotFSBuilder(fileSystem, s.fs.overlays, s.fs.overlays, s.fs.diskFiles, s.fs.diskDirectories, s.fs.nodeModulesRealpathAliases, store.options.PositionEncoding, store.toPath) - fileChanges = s.processFileChanges(fs, fileChanges, logger, nil) - - newSnapshotID := store.nextSnapshotID() - projectCollectionBuilder := newProjectCollectionBuilder( - ctx, - newSnapshotID, - fs, - s.ProjectCollection, - s.ConfigFileRegistry, - APIState{}, - compilerOptions, - s.inferredProjectContentMappers, - s.inferredProjectContentMapperExtensions, - store.options, - s.ConfigFileRegistry.customConfigFileName, - store.parseCache, - store.contentMappedParseCache, - store.extendedConfigCache, - store.contentMapperHost, - nil, - ) - - projectCollectionBuilder.seedInferredProjectForProgram(oldProject, logger) - if !fileChanges.IsEmpty() { - changeLogger := logger - if changeLogger != nil { - changeLogger = logger.Fork("DidChangeFiles") - } - projectCollectionBuilder.DidChangeFiles(fileChanges, changeLogger) - } - updateLogger := logger - if updateLogger != nil { - updateLogger = logger.Fork("UpdateProgramConfig") - } - projectCollectionBuilder.updateOrCreateInferredProject( - slices.Clone(rootFileNames), - compilerOptions, - projectReferences, - configFileParsingDiagnostics, - s.inferredProjectContentMappers, - updateLogger, - ) - if projectCollectionBuilder.inferredProject.Value().dirty { - createLogger := logger - if createLogger != nil { - createLogger = logger.Fork("CreateProgram") - } - projectCollectionBuilder.updateProgram(projectCollectionBuilder.inferredProject, createLogger) - } - projectCollectionBuilder.cleanupAllConfiguredProjects(logger.Fork("cleanupAllConfiguredProjects")) - newProjectCollection, newConfigFileRegistry := projectCollectionBuilder.Finalize(logger) - - cleanFilesStart := time.Now() - removedFiles := 0 - fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool { - for _, project := range newProjectCollection.Projects() { - if project.host != nil && project.host.sourceFS.SeenFile(entry.Key()) { - return true - } - } - entry.Delete() - removedFiles++ - return true - }) - if logger != nil { - logger.Logf("Removed %d cached file(s) in %v", removedFiles, time.Since(cleanFilesStart)) - } - - snapshotFS, _ := fs.Finalize() - newSnapshot := store.newSnapshot( - newSnapshotID, - snapshotFS, - newConfigFileRegistry, - compilerOptions, - s.userPreferences, - nil, - nil, - ) - newSnapshot.parentId = s.id - newSnapshot.ProjectCollection = newProjectCollection - newSnapshot.ConfigFileRegistry = newConfigFileRegistry - newSnapshot.inferredProjectContentMappers = s.inferredProjectContentMappers - newSnapshot.inferredProjectContentMapperExtensions = s.inferredProjectContentMapperExtensions - newSnapshot.builderLogs = logger - - for _, project := range newSnapshot.ProjectCollection.Projects() { - if project.Program != nil { - store.programCounter.Ref(project.Program) - if project.ProgramLastUpdate == newSnapshotID { - project.host.freeze(snapshotFS, newConfigFileRegistry) - } - } - } - - for _, config := range newSnapshot.ConfigFileRegistry.configs { - if config.commandLine != nil && config.commandLine.ConfigFile != nil { - for _, file := range config.commandLine.ConfigFile.ExtendedSourceFiles { - store.extendedConfigCache.AddOwner(store.toPath(file), newSnapshot.id) - } - } - } - - if logger != nil { - logger.Logf("Finished cloning snapshot %d into snapshot %d for program in %v", s.id, newSnapshot.id, time.Since(start)) - } - return newSnapshot +func (s *Snapshot) CreatedPrograms() []*Project { + return s.createdPrograms } func (s *Snapshot) cloneWithTemporaryFile( ctx context.Context, - fileSystem vfs.FS, uri lsproto.DocumentUri, newText string, ) (*Snapshot, error) { @@ -268,18 +134,24 @@ func (s *Snapshot) cloneWithTemporaryFile( fileChanges.Opened = uri } overlays[path] = newOverlay(uri.FileName(), newText, version, scriptKind) - if fileSystem == nil { - fileSystem = s.fs.fs - } return s.Clone(ctx, SnapshotChange{ - fs: fileSystem, + fs: s.fs.fs, fileSystemOverride: s.fileSystemOverride, fileChanges: fileChanges, - ResourceRequest: ResourceRequest{ - Documents: []lsproto.DocumentUri{uri}, - }, - }, overlays, nil), nil + ResourceRequest: s.resourceRequestForDocument(uri), + }, overlays, nil, nil), nil +} + +func (s *Snapshot) resourceRequestForDocument(uri lsproto.DocumentUri) ResourceRequest { + path := uri.Path(s.UseCaseSensitiveFileNames()) + request := ResourceRequest{Documents: []lsproto.DocumentUri{uri}} + for _, project := range s.ProjectCollection.SyntheticProjects() { + if project.containsFile(path) || project.host != nil && project.host.sourceFS.SeenFileOrMissingParentDirectory(path) { + request.Projects = append(request.Projects, project.ID()) + } + } + return request } func (s *Snapshot) processFileChanges( @@ -333,11 +205,13 @@ func (s *Snapshot) GetDefaultProject(uri lsproto.DocumentUri) *Project { return s.ProjectCollection.GetDefaultProject(uri.Path(s.UseCaseSensitiveFileNames())) } -func (s *Snapshot) GetProjectsContainingFile(uri lsproto.DocumentUri) []ls.Project { +// GetLanguageServiceProjectsContainingFile does not consider synthetic projects +// (ones created by API via createProgram) +func (s *Snapshot) GetLanguageServiceProjectsContainingFile(uri lsproto.DocumentUri) []ls.Project { fileName := uri.FileName() path := s.host.toPath(fileName) // TODO!! sheetal may be change this to handle symlinks!! - return s.ProjectCollection.GetProjectsContainingFile(path) + return s.ProjectCollection.GetLanguageServiceProjectsContainingFile(path) } func (s *Snapshot) GetFile(fileName string) FileHandle { @@ -386,11 +260,6 @@ func (s *Snapshot) UseCaseSensitiveFileNames() bool { return s.fs.fs.UseCaseSensitiveFileNames() } -// FileSystem returns the filesystem backing this snapshot. -func (s *Snapshot) FileSystem() vfs.FS { - return s.fs.fs -} - // HasFileSystemOverride reports whether this snapshot uses an API-supplied // filesystem instead of the session host filesystem. func (s *Snapshot) HasFileSystemOverride() bool { @@ -421,12 +290,24 @@ func (s *Snapshot) ReadDirectory(currentDir string, path string, extensions []st return vfsmatch.ReadDirectory(s.fs.fs, currentDir, path, extensions, excludes, includes, depth) } +type APICreateProgramRequest struct { + RootFileNames []string + CompilerOptions *core.CompilerOptions + ProjectReferences []*core.ProjectReference + ConfigFileParsingDiagnostics []*ast.Diagnostic +} + type APISnapshotRequest struct { - OpenProjects *collections.Set[string] - CloseProjects *collections.Set[tspath.Path] - OpenFiles *collections.Set[lsproto.DocumentUri] - CloseFiles *collections.Set[tspath.Path] - FileSystem vfs.FS + OpenProjects *collections.Set[string] + CloseProjects *collections.Set[tspath.Path] + OpenFiles *collections.Set[lsproto.DocumentUri] + CloseFiles *collections.Set[tspath.Path] + CreatePrograms []*APICreateProgramRequest + RemovePrograms *collections.Set[int] + EnsurePrograms *collections.Set[tspath.Path] + EnsureAllPrograms bool + EnsureFiles *collections.Set[lsproto.DocumentUri] + FileSystem vfs.FS // ReplaceFileSystem indicates a total filesystem replacement. Layers use // per-path file changes instead of invalidating all inherited state. ReplaceFileSystem bool @@ -491,7 +372,6 @@ type SnapshotChange struct { // ataChanges contains ATA-related changes to apply to projects in the new snapshot. ataChanges map[tspath.Path]*ATAStateChange apiRequest *APISnapshotRequest - client Client // cleanDiskCache triggers cleaning of cached disk files not referenced by any open project. cleanDiskCache bool } @@ -513,6 +393,7 @@ func (s *Snapshot) Clone( change SnapshotChange, overlays map[tspath.Path]*Overlay, sessionLogger logging.Logger, + client Client, ) *Snapshot { store := s.host var logger *logging.LogTree @@ -619,7 +500,7 @@ func (s *Snapshot) Clone( store.contentMappedParseCache, store.extendedConfigCache, store.contentMapperHost, - change.client, + client, ) if len(change.ataChanges) != 0 { @@ -763,6 +644,7 @@ func (s *Snapshot) Clone( newSnapshot.builderLogs = logger newSnapshot.apiError = apiError newSnapshot.fileSystemOverride = change.fileSystemOverride + newSnapshot.createdPrograms = projectCollectionBuilder.createdPrograms for _, project := range newSnapshot.ProjectCollection.Projects() { if project.Program != nil { diff --git a/tsc/internal/project/snapshot_test.go b/tsc/internal/project/snapshot_test.go index bda174fc061fd..da036c95e9685 100644 --- a/tsc/internal/project/snapshot_test.go +++ b/tsc/internal/project/snapshot_test.go @@ -6,6 +6,8 @@ import ( "testing" "github.com/microsoft/TypeScript/tsc/internal/bundled" + "github.com/microsoft/TypeScript/tsc/internal/collections" + "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" @@ -42,13 +44,143 @@ func TestSnapshot(t *testing.T) { baseSnapshot := session.Snapshot() uri := lsproto.DocumentUri("file:///temporary.ts") - snapshot, err := session.CloneSnapshotWithTemporaryFile(context.Background(), baseSnapshot, nil, uri, "export const value = 1;") + snapshot, err := session.CloneSnapshotWithTemporaryFile(context.Background(), baseSnapshot, uri, "export const value = 1;") assert.NilError(t, err) defer snapshot.Deref() assert.Equal(t, snapshot.GetFile(uri.FileName()).Content(), "export const value = 1;") }) + t.Run("creates and removes synthetic programs", func(t *testing.T) { + t.Parallel() + session := setup(map[string]any{ + "/a.ts": "export const a = 1;", + "/b.ts": "export const b = 1;", + }) + defer session.Close() + + ctx := context.Background() + options := &core.CompilerOptions{NoLib: core.TSTrue} + createRequest := &APISnapshotRequest{CreatePrograms: []*APICreateProgramRequest{ + { + RootFileNames: []string{"/a.ts"}, + CompilerOptions: options, + }, + { + RootFileNames: []string{"/b.ts"}, + CompilerOptions: options, + }, + }} + createdSnapshot, err := session.CloneSnapshot( + ctx, + session.Snapshot(), + FileChangeSummary{}, + createRequest, + ) + assert.NilError(t, err) + defer createdSnapshot.Deref() + assert.Equal(t, len(createdSnapshot.CreatedPrograms()), 2) + firstProject := createdSnapshot.CreatedPrograms()[0] + secondProject := createdSnapshot.CreatedPrograms()[1] + + firstProgramID, ok := SyntheticProgramID(firstProject.ID()) + assert.Assert(t, ok) + removeRequest := &APISnapshotRequest{RemovePrograms: collections.NewSetFromItems(firstProgramID)} + removedSnapshot, err := session.CloneSnapshot( + ctx, + createdSnapshot, + FileChangeSummary{}, + removeRequest, + ) + assert.NilError(t, err) + defer removedSnapshot.Deref() + + assert.Assert(t, firstProject != nil) + assert.Assert(t, secondProject != nil) + assert.Assert(t, firstProject.ID() != secondProject.ID()) + assert.DeepEqual(t, firstProject.CommandLine.FileNames(), []string{"/a.ts"}) + assert.DeepEqual(t, secondProject.CommandLine.FileNames(), []string{"/b.ts"}) + assert.Assert(t, createdSnapshot.ProjectCollection.InferredProject() == nil) + assert.Equal(t, len(createdSnapshot.ProjectCollection.SyntheticProjects()), 2) + assert.Equal(t, len(createdSnapshot.ProjectCollection.LanguageServiceProjects()), 0) + assert.Equal(t, len(createdSnapshot.GetLanguageServiceProjectsContainingFile(lsproto.DocumentUri("file:///a.ts"))), 0) + assert.Assert(t, createdSnapshot.ProjectCollection.GetDefaultProject(createdSnapshot.toPath("/a.ts")) == nil) + assert.Equal(t, createdSnapshot.ProjectCollection.GetProjectByPath(firstProject.ID()), firstProject) + + openedSnapshot, err := session.CloneSnapshot( + ctx, + createdSnapshot, + FileChangeSummary{}, + &APISnapshotRequest{OpenFiles: collections.NewSetFromItems(lsproto.DocumentUri("file:///a.ts"))}, + ) + assert.NilError(t, err) + defer openedSnapshot.Deref() + assert.Assert(t, openedSnapshot.ProjectCollection.InferredProject() != nil) + assert.Equal(t, len(openedSnapshot.ProjectCollection.LanguageServiceProjects()), 1) + assert.Equal(t, len(openedSnapshot.GetLanguageServiceProjectsContainingFile(lsproto.DocumentUri("file:///a.ts"))), 1) + assert.Equal(t, openedSnapshot.ProjectCollection.GetDefaultProject(openedSnapshot.toPath("/a.ts")), openedSnapshot.ProjectCollection.InferredProject()) + assert.Equal(t, openedSnapshot.ProjectCollection.GetProjectByPath(firstProject.ID()), firstProject) + + assert.Assert(t, removedSnapshot.ProjectCollection.GetProjectByPath(firstProject.ID()) == nil) + assert.Equal(t, removedSnapshot.ProjectCollection.GetProjectByPath(secondProject.ID()), secondProject) + assert.Equal(t, len(removedSnapshot.ProjectCollection.SyntheticProjects()), 1) + }) + + t.Run("document snapshots refresh default and synthetic programs", func(t *testing.T) { + t.Parallel() + const fileName = "/project/index.ts" + session := setup(map[string]any{ + "/project/tsconfig.json": `{}`, + fileName: "export const value = 1;", + }) + defer session.Close() + + ctx := context.Background() + uri := lsproto.DocumentUri("file://" + fileName) + baseSnapshot, err := session.CloneSnapshot( + ctx, + session.Snapshot(), + FileChangeSummary{}, + &APISnapshotRequest{ + OpenFiles: collections.NewSetFromItems(uri), + CreatePrograms: []*APICreateProgramRequest{{ + RootFileNames: []string{fileName}, + CompilerOptions: &core.CompilerOptions{NoLib: core.TSTrue}, + }}, + }, + ) + assert.NilError(t, err) + defer baseSnapshot.Deref() + + temporaryText := "export const value = 2;" + temporarySnapshot, err := session.CloneSnapshotWithTemporaryFile(ctx, baseSnapshot, uri, temporaryText) + assert.NilError(t, err) + defer temporarySnapshot.Deref() + + defaultProject := temporarySnapshot.GetDefaultProject(uri) + assert.Assert(t, defaultProject != nil) + assert.Equal(t, defaultProject.Program.GetSourceFile(fileName).Text(), temporaryText) + syntheticProject := temporarySnapshot.ProjectCollection.SyntheticProjects()[0] + assert.Equal(t, syntheticProject.Program.GetSourceFile(fileName).Text(), temporaryText) + + diskText := "export const value = 3;" + assert.NilError(t, session.fs.fs.WriteFile(fileName, diskText)) + var fileChanges FileChangeSummary + fileChanges.Changed.Add(uri) + dirtySnapshot, err := session.CloneSnapshot(ctx, baseSnapshot, fileChanges, nil) + assert.NilError(t, err) + defer dirtySnapshot.Deref() + assert.Assert(t, dirtySnapshot.GetDefaultProject(uri).IsDirty()) + assert.Assert(t, dirtySnapshot.ProjectCollection.SyntheticProjects()[0].IsDirty()) + + preparedSnapshot := session.SnapshotHost.CloneSnapshotWithAutoImports(ctx, dirtySnapshot, uri, nil) + defer preparedSnapshot.Deref() + defaultProject = preparedSnapshot.GetDefaultProject(uri) + assert.Equal(t, defaultProject.Program.GetSourceFile(fileName).Text(), diskText) + syntheticProject = preparedSnapshot.ProjectCollection.SyntheticProjects()[0] + assert.Equal(t, syntheticProject.Program.GetSourceFile(fileName).Text(), diskText) + }) + t.Run("compilerHost gets frozen with snapshot's FS only once", func(t *testing.T) { t.Parallel() files := map[string]any{ diff --git a/tsc/internal/project/snapshothost.go b/tsc/internal/project/snapshothost.go index faaa4c4321b20..4156197cc78ed 100644 --- a/tsc/internal/project/snapshothost.go +++ b/tsc/internal/project/snapshothost.go @@ -5,9 +5,7 @@ import ( "slices" "sync/atomic" - "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/contentmapper" - "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/project/logging" @@ -61,8 +59,8 @@ func NewSnapshotHost(init *SessionInit) *SnapshotHost { } } -// NewStandaloneRootSnapshot creates the compatibility root for a standalone API session. -func (s *SnapshotHost) NewStandaloneRootSnapshot() *Snapshot { +// NewRootSnapshot creates an independent root snapshot. +func (s *SnapshotHost) NewRootSnapshot() *Snapshot { return s.newRootSnapshot(0, false) } @@ -95,57 +93,28 @@ func (s *SnapshotHost) CloneSnapshot( // update derives a snapshot from baseSnapshot without adopting it as any // canonical session state or performing session side effects. func (s *SnapshotHost) update(ctx context.Context, baseSnapshot *Snapshot, change SnapshotChange) *Snapshot { - return baseSnapshot.Clone(ctx, change, baseSnapshot.fs.overlays, nil) + return baseSnapshot.Clone(ctx, change, baseSnapshot.fs.overlays, nil, nil) } // CloneSnapshotWithTemporaryFile derives a snapshot with a temporary file content override. func (s *SnapshotHost) CloneSnapshotWithTemporaryFile( ctx context.Context, baseSnapshot *Snapshot, - fileSystem vfs.FS, uri lsproto.DocumentUri, newText string, ) (*Snapshot, error) { - return baseSnapshot.cloneWithTemporaryFile(ctx, fileSystem, uri, newText) -} - -// CloneSnapshotForProgram derives an isolated snapshot containing one synthetic -// project. The base snapshot is not adopted as canonical state. -func (s *SnapshotHost) CloneSnapshotForProgram( - ctx context.Context, - baseSnapshot *Snapshot, - fileSystem vfs.FS, - rootFileNames []string, - options *core.CompilerOptions, - projectReferences []*core.ProjectReference, - configFileParsingDiagnostics []*ast.Diagnostic, - oldProject *Project, - fileChanges FileChangeSummary, -) *Snapshot { - return baseSnapshot.cloneForProgram( - ctx, - fileSystem, - rootFileNames, - options, - projectReferences, - configFileParsingDiagnostics, - oldProject, - fileChanges, - nil, - ) + return baseSnapshot.cloneWithTemporaryFile(ctx, uri, newText) } // CloneSnapshotWithAutoImports derives a snapshot with auto-import preparation without // adopting the clone in the background. func (s *SnapshotHost) CloneSnapshotWithAutoImports(ctx context.Context, baseSnapshot *Snapshot, uri lsproto.DocumentUri, logger logging.Logger) *Snapshot { change := SnapshotChange{ - reason: UpdateReasonRequestedLanguageServiceWithAutoImports, - ResourceRequest: ResourceRequest{ - Documents: []lsproto.DocumentUri{uri}, - AutoImports: uri, - }, + reason: UpdateReasonRequestedLanguageServiceWithAutoImports, + ResourceRequest: baseSnapshot.resourceRequestForDocument(uri), } - return baseSnapshot.Clone(ctx, change, baseSnapshot.fs.overlays, logger) + change.AutoImports = uri + return baseSnapshot.Clone(ctx, change, baseSnapshot.fs.overlays, logger, nil) } func (s *SnapshotHost) newRootSnapshot(id uint64, relativePatternSupport bool) *Snapshot {