Skip to content

Snapshot LSP #1505

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 103 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
2009395
WIP
andrewbranch Jun 20, 2025
9f87816
WIP
andrewbranch Jun 23, 2025
bd7bf26
WIP
andrewbranch Jun 24, 2025
418b370
WIP
andrewbranch Jun 24, 2025
254067c
Scaffold up to getting a language service
andrewbranch Jun 24, 2025
e1b8f25
WIP config registry and project finding
andrewbranch Jun 26, 2025
297f2aa
Builders
andrewbranch Jun 27, 2025
123930d
WIP
andrewbranch Jun 27, 2025
6222a94
WIP
andrewbranch Jun 30, 2025
707f1fd
WIP
andrewbranch Jun 30, 2025
a634820
Merge branch 'main' into snapshots
andrewbranch Jun 30, 2025
e11f6e8
Extended config cache
andrewbranch Jul 2, 2025
067b005
Inferred projects
andrewbranch Jul 3, 2025
13a6b5b
Split compiler host, implement parallel BFS
andrewbranch Jul 8, 2025
a82168a
Switch builder to use BFS
andrewbranch Jul 9, 2025
0b2cd22
WIP tests
andrewbranch Jul 10, 2025
3308b94
WIP
andrewbranch Jul 10, 2025
dca8946
Dirty map helpers
andrewbranch Jul 10, 2025
4bf7c22
Move dirty maps to package, first test passing
andrewbranch Jul 11, 2025
14b83c2
All project finder tests passing
andrewbranch Jul 11, 2025
688b6d5
Port project lifetime tests and handle removing files from inferred p…
andrewbranch Jul 14, 2025
429399d
Port project references tests
andrewbranch Jul 15, 2025
3cd5cc8
Ref counting caches
andrewbranch Jul 15, 2025
85e7a20
Merge branch 'main' into snapshots
andrewbranch Jul 15, 2025
ef4111d
Merge branch 'main' into snapshots
andrewbranch Jul 15, 2025
b8231e2
Use script kind from overlay
andrewbranch Jul 16, 2025
9a332f5
Fix merge
andrewbranch Jul 16, 2025
e545081
Deduplicate file changes
andrewbranch Jul 16, 2025
723c6da
Config file changes
andrewbranch Jul 17, 2025
58d3344
More watch
andrewbranch Jul 22, 2025
3638d08
Watch requests
andrewbranch Jul 22, 2025
ae3effa
Add remaining tests
andrewbranch Jul 23, 2025
4682c38
Start adding logs
andrewbranch Jul 23, 2025
13a7ade
Cache files between snapshots
andrewbranch Jul 24, 2025
5c8af90
Merge branch 'main' into snapshots
andrewbranch Jul 24, 2025
f6f0682
WIP
andrewbranch Jul 25, 2025
85421fd
Project logging
andrewbranch Jul 25, 2025
f0a1e24
ATA
andrewbranch Jul 29, 2025
e47e9f5
ATA tests passing
andrewbranch Jul 29, 2025
4f81d10
Improve logging
andrewbranch Jul 30, 2025
b16fb53
Fix logger duplicate closing
andrewbranch Jul 30, 2025
1d9ece1
Minimize loops over configs and projects
andrewbranch Jul 30, 2025
ff964c6
Simplify background queue to fix racy tests
andrewbranch Jul 30, 2025
535fdcc
Try glob matching instead of MatchesFileName
andrewbranch Jul 30, 2025
cc954f6
More logging
andrewbranch Jul 31, 2025
ef47bce
Fix race condition on diskFile
andrewbranch Jul 31, 2025
b686d8d
Remove custom logic for asserting watch files calls
andrewbranch Jul 31, 2025
ee3b24e
Add missing nil check
andrewbranch Jul 31, 2025
798c4d1
Port discovertypings_test
andrewbranch Jul 31, 2025
256456f
Move ATA to own package
andrewbranch Jul 31, 2025
702f70c
Logging refactors
andrewbranch Jul 31, 2025
9138f87
Move dirty into projectv2
andrewbranch Aug 1, 2025
41a50ed
Merge branch 'main' into snapshots
andrewbranch Aug 1, 2025
8d0043e
Integrate into real LSP server
andrewbranch Aug 1, 2025
5c569de
Fix race, replace sha256
andrewbranch Aug 2, 2025
a6f5cea
Delete and rename
andrewbranch Aug 2, 2025
dd661db
Delete and port LS tests
andrewbranch Aug 4, 2025
e0f5832
Make API build with TODOs
andrewbranch Aug 4, 2025
1ee7cf9
Start fixing other tests
andrewbranch Aug 4, 2025
612160b
Tests passing
andrewbranch Aug 4, 2025
3f06a02
Rename copilot-named method
andrewbranch Aug 4, 2025
4bb9cc9
Merge branch 'main' into snapshots
andrewbranch Aug 4, 2025
4841c7a
Merge branch 'main' into snapshots
andrewbranch Aug 4, 2025
ed7fa9f
Fix merge conflict resolution
andrewbranch Aug 4, 2025
fe2622b
Revert unnecessary changes
andrewbranch Aug 4, 2025
8a424fe
Re-add installnpmpackages_test
andrewbranch Aug 4, 2025
918ac91
Revert unnecessary changes
andrewbranch Aug 4, 2025
7aa17f8
Eagerly bind files
andrewbranch Aug 4, 2025
6f57299
Merge branch 'main' into snapshots
andrewbranch Aug 4, 2025
815c282
Rename CopyMaps
andrewbranch Aug 4, 2025
3217593
Delete DocumentURIToFileName
andrewbranch Aug 4, 2025
42bf293
Standardize on cache "Deref" method
andrewbranch Aug 4, 2025
7b67bd1
Fix Program sync.Once
andrewbranch Aug 4, 2025
1d65270
Rename `recover` local
andrewbranch Aug 4, 2025
5b6c954
Fix potential SyncMapEntry race
andrewbranch Aug 4, 2025
beade80
Fix lints
andrewbranch Aug 4, 2025
c7dd280
Only pass typingsLocation to program if ATA enabled for project
andrewbranch Aug 4, 2025
021949f
More fix dirty.SyncMap
andrewbranch Aug 5, 2025
cdee81c
Use assert.NilError
andrewbranch Aug 5, 2025
63cb15e
Skip API tests for now
andrewbranch Aug 5, 2025
e9d8d32
Make default logger concurrency safe
andrewbranch Aug 5, 2025
6200135
Fix ATA thrashing
andrewbranch Aug 5, 2025
a9155d2
Fix LS panic recovery
andrewbranch Aug 5, 2025
7f1cccd
Fix watcher race
andrewbranch Aug 5, 2025
d73bb33
Fix race in BFS test
andrewbranch Aug 5, 2025
ecc84ad
Delete interface witness utility type
andrewbranch Aug 5, 2025
f65520c
Minimize snapshot updates on saves
andrewbranch Aug 5, 2025
5dd062a
Fix data race on SyncMapEntry.delete between LoadOrStore() read and D…
andrewbranch Aug 5, 2025
1c94605
Disable LSP textChange/didSave includeText
andrewbranch Aug 5, 2025
eb4dc53
Move backgroundqueue to package
andrewbranch Aug 5, 2025
11d7ce7
Fix lints
andrewbranch Aug 5, 2025
c84c12d
Wire up API
andrewbranch Aug 6, 2025
bf4c801
Fix API crashes
andrewbranch Aug 6, 2025
c157e6c
Add more logging
andrewbranch Aug 6, 2025
04873dd
Delete unused SessionHooks
andrewbranch Aug 6, 2025
8955564
Fix logger race
andrewbranch Aug 6, 2025
b2ba79d
Fix compilerHost freeze race
andrewbranch Aug 6, 2025
abdf6c8
Simplify ExtractUnresolvedImports
andrewbranch Aug 6, 2025
1cbc94f
program ref was in wrong if block
andrewbranch Aug 6, 2025
ff55acc
Test and actually fix the compilerHost freeze
andrewbranch Aug 6, 2025
773ec61
Fix usage of nil unresolvedImports after program change
andrewbranch Aug 6, 2025
4c4ee5d
Lint
andrewbranch Aug 6, 2025
1028aba
Merge branch 'main' into snapshots
jakebailey Aug 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 63 additions & 111 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,27 @@ import (
"github.com/microsoft/typescript-go/internal/astnav"
"github.com/microsoft/typescript-go/internal/checker"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/ls"
"github.com/microsoft/typescript-go/internal/project"
"github.com/microsoft/typescript-go/internal/project/logging"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
)

type handleMap[T any] map[Handle[T]]*T

type APIOptions struct {
Logger *project.Logger
type APIInit struct {
Logger logging.Logger
FS vfs.FS
SessionOptions *project.SessionOptions
}

type API struct {
host APIHost
options APIOptions
logger logging.Logger
session *project.Session

documentStore *project.DocumentStore
configFileRegistry *project.ConfigFileRegistry

projects handleMap[project.Project]
projects map[Handle[project.Project]]tspath.Path
filesMu sync.Mutex
files handleMap[ast.SourceFile]
symbolsMu sync.Mutex
Expand All @@ -41,91 +41,22 @@ type API struct {
types handleMap[checker.Type]
}

var _ project.ProjectHost = (*API)(nil)

func NewAPI(host APIHost, options APIOptions) *API {
func NewAPI(init *APIInit) *API {
api := &API{
host: host,
options: options,
projects: make(handleMap[project.Project]),
session: project.NewSession(&project.SessionInit{
Logger: init.Logger,
FS: init.FS,
Options: init.SessionOptions,
}),
projects: make(map[Handle[project.Project]]tspath.Path),
files: make(handleMap[ast.SourceFile]),
symbols: make(handleMap[ast.Symbol]),
types: make(handleMap[checker.Type]),
}

api.documentStore = project.NewDocumentStore(project.DocumentStoreOptions{
ComparePathsOptions: tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(),
CurrentDirectory: host.GetCurrentDirectory(),
},
Hooks: project.DocumentRegistryHooks{
OnReleaseDocument: func(file *ast.SourceFile) {
_ = api.releaseHandle(string(FileHandle(file)))
},
},
})

api.configFileRegistry = &project.ConfigFileRegistry{
Host: api,
}
return api
}

// DefaultLibraryPath implements ProjectHost.
func (api *API) DefaultLibraryPath() string {
return api.host.DefaultLibraryPath()
}

// TypingsInstaller implements ProjectHost
func (api *API) TypingsInstaller() *project.TypingsInstaller {
return nil
}

// DocumentStore implements ProjectHost.
func (api *API) DocumentStore() *project.DocumentStore {
return api.documentStore
}

// ConfigFileRegistry implements ProjectHost.
func (api *API) ConfigFileRegistry() *project.ConfigFileRegistry {
return api.configFileRegistry
}

// FS implements ProjectHost.
func (api *API) FS() vfs.FS {
return api.host.FS()
}

// GetCurrentDirectory implements ProjectHost.
func (api *API) GetCurrentDirectory() string {
return api.host.GetCurrentDirectory()
}

// Log implements ProjectHost.
func (api *API) Log(s string) {
api.options.Logger.Info(s)
}

// Log implements ProjectHost.
func (api *API) Trace(s string) {
api.options.Logger.Info(s)
}

// PositionEncoding implements ProjectHost.
func (api *API) PositionEncoding() lsproto.PositionEncodingKind {
return lsproto.PositionEncodingKindUTF8
}

// Client implements ProjectHost.
func (api *API) Client() project.Client {
return nil
}

// IsWatchEnabled implements ProjectHost.
func (api *API) IsWatchEnabled() bool {
return false
}

func (api *API) HandleRequest(ctx context.Context, method string, payload []byte) ([]byte, error) {
params, err := unmarshalPayload(method, payload)
if err != nil {
Expand All @@ -149,7 +80,7 @@ func (api *API) HandleRequest(ctx context.Context, method string, payload []byte
case MethodParseConfigFile:
return encodeJSON(api.ParseConfigFile(params.(*ParseConfigFileParams).FileName))
case MethodLoadProject:
return encodeJSON(api.LoadProject(params.(*LoadProjectParams).ConfigFileName))
return encodeJSON(api.LoadProject(ctx, params.(*LoadProjectParams).ConfigFileName))
case MethodGetSymbolAtPosition:
params := params.(*GetSymbolAtPositionParams)
return encodeJSON(api.GetSymbolAtPosition(ctx, params.Project, params.FileName, int(params.Position)))
Expand Down Expand Up @@ -180,20 +111,20 @@ func (api *API) HandleRequest(ctx context.Context, method string, payload []byte
}

func (api *API) Close() {
api.options.Logger.Close()
api.session.Close()
}

func (api *API) ParseConfigFile(configFileName string) (*ConfigFileResponse, error) {
configFileName = api.toAbsoluteFileName(configFileName)
configFileContent, ok := api.host.FS().ReadFile(configFileName)
configFileContent, ok := api.session.FS().ReadFile(configFileName)
if !ok {
return nil, fmt.Errorf("could not read file %q", configFileName)
}
configDir := tspath.GetDirectoryPath(configFileName)
tsConfigSourceFile := tsoptions.NewTsconfigSourceFileFromFilePath(configFileName, api.toPath(configFileName), configFileContent)
parsedCommandLine := tsoptions.ParseJsonSourceFileConfigFileContent(
tsConfigSourceFile,
api.host,
api.session,
configDir,
nil, /*existingOptions*/
configFileName,
Expand All @@ -207,26 +138,29 @@ func (api *API) ParseConfigFile(configFileName string) (*ConfigFileResponse, err
}, nil
}

func (api *API) LoadProject(configFileName string) (*ProjectResponse, error) {
configFileName = api.toAbsoluteFileName(configFileName)
configFilePath := api.toPath(configFileName)
p := project.NewConfiguredProject(configFileName, configFilePath, api)
if err := p.LoadConfig(); err != nil {
func (api *API) LoadProject(ctx context.Context, configFileName string) (*ProjectResponse, error) {
project, err := api.session.OpenProject(ctx, api.toAbsoluteFileName(configFileName))
if err != nil {
return nil, err
}
p.GetProgram()
data := NewProjectResponse(p)
api.projects[data.Id] = p
data := NewProjectResponse(project)
api.projects[data.Id] = project.ConfigFilePath()
return data, nil
}

func (api *API) GetSymbolAtPosition(ctx context.Context, projectId Handle[project.Project], fileName string, position int) (*SymbolResponse, error) {
project, ok := api.projects[projectId]
projectPath, ok := api.projects[projectId]
if !ok {
return nil, errors.New("project ID not found")
}
snapshot, release := api.session.Snapshot()
defer release()
project := snapshot.ProjectCollection.GetProjectByPath(projectPath)
if project == nil {
return nil, errors.New("project not found")
}
languageService, done := project.GetLanguageServiceForRequest(ctx)
defer done()

languageService := ls.NewLanguageService(project, snapshot.Converters())
symbol, err := languageService.GetSymbolAtPosition(ctx, fileName, position)
if err != nil || symbol == nil {
return nil, err
Expand All @@ -239,10 +173,17 @@ func (api *API) GetSymbolAtPosition(ctx context.Context, projectId Handle[projec
}

func (api *API) GetSymbolAtLocation(ctx context.Context, projectId Handle[project.Project], location Handle[ast.Node]) (*SymbolResponse, error) {
project, ok := api.projects[projectId]
projectPath, ok := api.projects[projectId]
if !ok {
return nil, errors.New("project ID not found")
}
snapshot, release := api.session.Snapshot()
defer release()
project := snapshot.ProjectCollection.GetProjectByPath(projectPath)
if project == nil {
return nil, errors.New("project not found")
}

fileHandle, pos, kind, err := parseNodeHandle(location)
if err != nil {
return nil, err
Expand All @@ -261,8 +202,7 @@ func (api *API) GetSymbolAtLocation(ctx context.Context, projectId Handle[projec
if node == nil {
return nil, fmt.Errorf("node of kind %s not found at position %d in file %q", kind.String(), pos, sourceFile.FileName())
}
languageService, done := project.GetLanguageServiceForRequest(ctx)
defer done()
languageService := ls.NewLanguageService(project, snapshot.Converters())
symbol := languageService.GetSymbolAtLocation(ctx, node)
if symbol == nil {
return nil, nil
Expand All @@ -275,18 +215,24 @@ func (api *API) GetSymbolAtLocation(ctx context.Context, projectId Handle[projec
}

func (api *API) GetTypeOfSymbol(ctx context.Context, projectId Handle[project.Project], symbolHandle Handle[ast.Symbol]) (*TypeResponse, error) {
project, ok := api.projects[projectId]
projectPath, ok := api.projects[projectId]
if !ok {
return nil, errors.New("project ID not found")
}
snapshot, release := api.session.Snapshot()
defer release()
project := snapshot.ProjectCollection.GetProjectByPath(projectPath)
if project == nil {
return nil, errors.New("project not found")
}

api.symbolsMu.Lock()
defer api.symbolsMu.Unlock()
symbol, ok := api.symbols[symbolHandle]
if !ok {
return nil, fmt.Errorf("symbol %q not found", symbolHandle)
}
languageService, done := project.GetLanguageServiceForRequest(ctx)
defer done()
languageService := ls.NewLanguageService(project, snapshot.Converters())
t := languageService.GetTypeOfSymbol(ctx, symbol)
if t == nil {
return nil, nil
Expand All @@ -295,10 +241,17 @@ func (api *API) GetTypeOfSymbol(ctx context.Context, projectId Handle[project.Pr
}

func (api *API) GetSourceFile(projectId Handle[project.Project], fileName string) (*ast.SourceFile, error) {
project, ok := api.projects[projectId]
projectPath, ok := api.projects[projectId]
if !ok {
return nil, errors.New("project ID not found")
}
snapshot, release := api.session.Snapshot()
defer release()
project := snapshot.ProjectCollection.GetProjectByPath(projectPath)
if project == nil {
return nil, errors.New("project not found")
}

sourceFile := project.GetProgram().GetSourceFile(fileName)
if sourceFile == nil {
return nil, fmt.Errorf("source file %q not found", fileName)
Expand All @@ -313,12 +266,11 @@ func (api *API) releaseHandle(handle string) error {
switch handle[0] {
case handlePrefixProject:
projectId := Handle[project.Project](handle)
project, ok := api.projects[projectId]
_, ok := api.projects[projectId]
if !ok {
return fmt.Errorf("project %q not found", handle)
}
delete(api.projects, projectId)
project.Close()
case handlePrefixFile:
fileId := Handle[ast.SourceFile](handle)
api.filesMu.Lock()
Expand Down Expand Up @@ -353,11 +305,11 @@ func (api *API) releaseHandle(handle string) error {
}

func (api *API) toAbsoluteFileName(fileName string) string {
return tspath.GetNormalizedAbsolutePath(fileName, api.host.GetCurrentDirectory())
return tspath.GetNormalizedAbsolutePath(fileName, api.session.GetCurrentDirectory())
}

func (api *API) toPath(fileName string) tspath.Path {
return tspath.ToPath(fileName, api.host.GetCurrentDirectory(), api.host.FS().UseCaseSensitiveFileNames())
return tspath.ToPath(fileName, api.session.GetCurrentDirectory(), api.session.FS().UseCaseSensitiveFileNames())
}

func encodeJSON(v any, err error) ([]byte, error) {
Expand Down
9 changes: 0 additions & 9 deletions internal/api/host.go

This file was deleted.

4 changes: 2 additions & 2 deletions internal/api/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ func NewProjectResponse(project *project.Project) *ProjectResponse {
return &ProjectResponse{
Id: ProjectHandle(project),
ConfigFileName: project.Name(),
RootFiles: project.GetRootFileNames(),
CompilerOptions: project.GetCompilerOptions(),
RootFiles: project.CommandLine.FileNames(),
CompilerOptions: project.CommandLine.CompilerOptions(),
}
}

Expand Down
Loading
Loading