You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The LSP-connected API adopts the language server's canonical state through an empty updateSnapshot() call.
These operations grew from different requirements and do not form one coherent state-transition model. In particular:
A standalone API client does not inherently have a meaningful "latest" snapshot. Only an LSP session has canonical current state. Currently, if you call api.createProgram without passing an oldProgram, the behavior implicitly depends on what's in the "latest snapshot." It looks totally unexpected that you could get different results at different times because of intermediate calls.
fileChanges describes out-of-band mutations rather than performing a state change. It is redundant for in-band virtual filesystem updates that you can do with Add optional VFS parameters to updateSnapshot #64115 and awkward as the default way to obtain TS 6-style incremental behavior.
createProgram taking an oldProgram as the source for the base snapshot to clone from means you can't have a sequence like create a program (S1), perform a file system update (S2), then update the program from S1 while keeping the file system changes from S2. On the other hand, allowing createProgram to be based off of an arbitrary snapshot while also taking an arbitrary oldProgram means the server doesn't know the relationship between the base snapshot and the snapshot that produced oldProgram, so oldProgram could never be reused.
createProgram currently consumes the one inferred-project slot, preventing a snapshot from containing multiple independently API-created programs. (Having multiple programs in a single snapshot can be desirable if they overlap in source files, since binder symbols have referential equality within the same snapshot.)
Redesign
Clarifying the initial state and removing "latest" state
First, I want to replace api.updateSnapshot with two different methods that make it a little more clear what you're getting:
// Only available in LSP-connected API!// Gets the canonical state of the LSP server:consts=api.getLatestSnapshot();// Maybe even name it `getLatestLSPSnapshot()`?// Available in both LSP mode and standalone mode:consts=api.getEmptySnapshot();
The API client will have no concept of a "latest snapshot"; you can get the LSP server's latest snapshot if you're connected to the LSP.
Updating a snapshot
Any snapshot can be updated with changes passed to snapshot.update(), and everything you can do to clone a snapshot with changes should be expressible through that API, including creating/updating/removing a program, opening/closing a tsconfig project, applying virtual file system overlays, etc.:
getEmptySnapshot (and getLatestSnapshot) can also take a set of initial changes to incorporate (there's no reason to round-trip to get an empty snapshot if you're going to add to it in another request right afterwards). Maybe we should name that method getInitialSnapshot or something, since it's confusing to request an empty snapshot with stuff in it:
consts0=api.getEmptySnapshot().update({/* ... */});// better written as:consts0=api.getInitialSnapshot({/* ... */});
Or perhaps api.createSnapshot() encodes the meaning well enough whether or not it has arguments:
consts0=api.createSnapshot();// empty, not a ton of reason to do thisconsts0=api.createSnapshot({createPrograms: [/* ... */]});
File system updates shouldn't automatically refresh affected projects/programs; we need to allow the API client to specify what it needs:
consts0=api.getInitialSnapshot({createPrograms: [{ rootFiles, options }]});// No program update performed, but program is marked dirty if affectedconsts1=s0.update({fileSystem: /* ... */});letprogram=s1.programs[0];program.dirty;// true// Updates the program if dirtyconsts2=s1.update({ensurePrograms: [program.id]});program=s2.programs[0];program.dirty;// false
(We may want to have createProgram pass a program id and look it up from a map rather than autogenerating one and accessing programs in an array.)
This solves the problem of combining efficient updates of existing programs with other snapshot changes without introducing the possibility of grafting a program from one snapshot into an unrelated one with unpredictable results. (It loses the ability to “move” a tsconfig project’s program into an isolated snapshot, but that was sort of a side effect of the current design rather than a use case we were designing for.)
Convenience APIs
One motivation of #63950 was to hide snapshot management for API users who only need to create one program, or multiple disconnected programs, and never need to update them over time. api.createProgram should still exist, but it can be sugar over the lower-level snapshot update API:
I'm undecided whether we should expose a way to update that program without going back out to the snapshot.update API. I think it would be reasonable to say that if you want to do anything that evolves a non-initial snapshot, you should get comfortable using snapshot.update.
Architectural changes
We need to change a few things on the server side to make this happen.
The api.Session really doesn't need to run its snapshot updating work through project.Session unless it's in LSP-connection mode. That will remove the concept of a latest/canonical snapshot from the API server when running in standalone mode. We should extract the parts of project.Session that own global caches, i.e. the interface that snapshot.Clone relies on, into an object that's separate from the project.Session's ownership of a single latest state and reactions to updating that state. In standalone mode, the api.Session can fulfill that interface or own an object implementing that interface rather than owning a full project.Session.
We need to use a different storage mechanism for API-created programs than the single inferred project of project.ProjectCollection. Probably a separate map of projects, leaving both configured and inferred projects alone. (This is needed both to support multiple API-created program in the same snapshot as well as adding a single API-created program to an LSP snapshot where there may already be an inferred project.)
These are both moderately sized refactors, but I think they'll actually simplify the mental model, and everything else should fall cleanly out of these moves.
Context
The API currently exposes several overlapping ways to describe state changes:
api.updateSnapshot({ fileChanges })applies caller-reported changes to the API client's latest snapshot.api.createProgram(..., oldProgram, fileChanges)derives a program from the snapshot that ownsoldProgram.snapshot.update({ fileSystem }), which layers a virtual filesystem over a snapshot.updateSnapshot()call.These operations grew from different requirements and do not form one coherent state-transition model. In particular:
api.createProgramwithout passing anoldProgram, the behavior implicitly depends on what's in the "latest snapshot." It looks totally unexpected that you could get different results at different times because of intermediate calls.fileChangesdescribes out-of-band mutations rather than performing a state change. It is redundant for in-band virtual filesystem updates that you can do with Add optional VFS parameters to updateSnapshot #64115 and awkward as the default way to obtain TS 6-style incremental behavior.createProgramtaking anoldProgramas the source for the base snapshot to clone from means you can't have a sequence like create a program (S1), perform a file system update (S2), then update the program from S1 while keeping the file system changes from S2. On the other hand, allowingcreateProgramto be based off of an arbitrary snapshot while also taking an arbitraryoldProgrammeans the server doesn't know the relationship between the base snapshot and the snapshot that producedoldProgram, sooldProgramcould never be reused.createProgramcurrently consumes the one inferred-project slot, preventing a snapshot from containing multiple independently API-created programs. (Having multiple programs in a single snapshot can be desirable if they overlap in source files, since binder symbols have referential equality within the same snapshot.)Redesign
Clarifying the initial state and removing "latest" state
First, I want to replace
api.updateSnapshotwith two different methods that make it a little more clear what you're getting:The API client will have no concept of a "latest snapshot"; you can get the LSP server's latest snapshot if you're connected to the LSP.
Updating a snapshot
Any snapshot can be updated with changes passed to
snapshot.update(), and everything you can do to clone a snapshot with changes should be expressible through that API, including creating/updating/removing a program, opening/closing a tsconfig project, applying virtual file system overlays, etc.:getEmptySnapshot(andgetLatestSnapshot) can also take a set of initial changes to incorporate (there's no reason to round-trip to get an empty snapshot if you're going to add to it in another request right afterwards). Maybe we should name that methodgetInitialSnapshotor something, since it's confusing to request an empty snapshot with stuff in it:Or perhaps
api.createSnapshot()encodes the meaning well enough whether or not it has arguments:File system updates shouldn't automatically refresh affected projects/programs; we need to allow the API client to specify what it needs:
(We may want to have
createProgrampass a program id and look it up from a map rather than autogenerating one and accessing programs in an array.)This solves the problem of combining efficient updates of existing programs with other snapshot changes without introducing the possibility of grafting a program from one snapshot into an unrelated one with unpredictable results. (It loses the ability to “move” a tsconfig project’s program into an isolated snapshot, but that was sort of a side effect of the current design rather than a use case we were designing for.)
Convenience APIs
One motivation of #63950 was to hide snapshot management for API users who only need to create one program, or multiple disconnected programs, and never need to update them over time.
api.createProgramshould still exist, but it can be sugar over the lower-level snapshot update API:I'm undecided whether we should expose a way to update that program without going back out to the
snapshot.updateAPI. I think it would be reasonable to say that if you want to do anything that evolves a non-initial snapshot, you should get comfortable usingsnapshot.update.Architectural changes
We need to change a few things on the server side to make this happen.
api.Sessionreally doesn't need to run its snapshot updating work throughproject.Sessionunless it's in LSP-connection mode. That will remove the concept of a latest/canonical snapshot from the API server when running in standalone mode. We should extract the parts ofproject.Sessionthat own global caches, i.e. the interface thatsnapshot.Clonerelies on, into an object that's separate from theproject.Session's ownership of a single latest state and reactions to updating that state. In standalone mode, theapi.Sessioncan fulfill that interface or own an object implementing that interface rather than owning a fullproject.Session.project.ProjectCollection. Probably a separate map of projects, leaving both configured and inferred projects alone. (This is needed both to support multiple API-created program in the same snapshot as well as adding a single API-created program to an LSP snapshot where there may already be an inferred project.)These are both moderately sized refactors, but I think they'll actually simplify the mental model, and everything else should fall cleanly out of these moves.