2025-10-15 10:12:44 +03:00

321 lines
10 KiB
Go

package api
import (
"context"
"errors"
"fmt"
"sync"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/api/encoder"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ast"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/astnav"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/checker"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/core"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/ls"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/project/logging"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tsoptions"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath"
"efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs"
"github.com/go-json-experiment/json"
)
type handleMap[T any] map[Handle[T]]*T
type APIInit struct {
Logger logging.Logger
FS vfs.FS
SessionOptions *project.SessionOptions
}
type API struct {
logger logging.Logger
session *project.Session
projects map[Handle[project.Project]]tspath.Path
filesMu sync.Mutex
files handleMap[ast.SourceFile]
symbolsMu sync.Mutex
symbols handleMap[ast.Symbol]
typesMu sync.Mutex
types handleMap[checker.Type]
}
func NewAPI(init *APIInit) *API {
api := &API{
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]),
}
return api
}
func (api *API) HandleRequest(ctx context.Context, method string, payload []byte) ([]byte, error) {
params, err := unmarshalPayload(method, payload)
if err != nil {
return nil, err
}
switch Method(method) {
case MethodRelease:
if id, ok := params.(*string); ok {
return nil, api.releaseHandle(*id)
} else {
return nil, fmt.Errorf("expected string for release handle, got %T", params)
}
case MethodGetSourceFile:
params := params.(*GetSourceFileParams)
sourceFile, err := api.GetSourceFile(params.Project, params.FileName)
if err != nil {
return nil, err
}
return encoder.EncodeSourceFile(sourceFile, string(FileHandle(sourceFile)))
case MethodParseConfigFile:
return encodeJSON(api.ParseConfigFile(params.(*ParseConfigFileParams).FileName))
case MethodLoadProject:
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)))
case MethodGetSymbolsAtPositions:
params := params.(*GetSymbolsAtPositionsParams)
return encodeJSON(core.TryMap(params.Positions, func(position uint32) (any, error) {
return api.GetSymbolAtPosition(ctx, params.Project, params.FileName, int(position))
}))
case MethodGetSymbolAtLocation:
params := params.(*GetSymbolAtLocationParams)
return encodeJSON(api.GetSymbolAtLocation(ctx, params.Project, params.Location))
case MethodGetSymbolsAtLocations:
params := params.(*GetSymbolsAtLocationsParams)
return encodeJSON(core.TryMap(params.Locations, func(location Handle[ast.Node]) (any, error) {
return api.GetSymbolAtLocation(ctx, params.Project, location)
}))
case MethodGetTypeOfSymbol:
params := params.(*GetTypeOfSymbolParams)
return encodeJSON(api.GetTypeOfSymbol(ctx, params.Project, params.Symbol))
case MethodGetTypesOfSymbols:
params := params.(*GetTypesOfSymbolsParams)
return encodeJSON(core.TryMap(params.Symbols, func(symbol Handle[ast.Symbol]) (any, error) {
return api.GetTypeOfSymbol(ctx, params.Project, symbol)
}))
default:
return nil, fmt.Errorf("unhandled API method %q", method)
}
}
func (api *API) Close() {
api.session.Close()
}
func (api *API) ParseConfigFile(configFileName string) (*ConfigFileResponse, error) {
configFileName = api.toAbsoluteFileName(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.session,
configDir,
nil, /*existingOptions*/
configFileName,
nil, /*resolutionStack*/
nil, /*extraFileExtensions*/
nil, /*extendedConfigCache*/
)
return &ConfigFileResponse{
FileNames: parsedCommandLine.FileNames(),
Options: parsedCommandLine.CompilerOptions(),
}, 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
}
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) {
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 := ls.NewLanguageService(project.GetProgram(), snapshot)
symbol, err := languageService.GetSymbolAtPosition(ctx, fileName, position)
if err != nil || symbol == nil {
return nil, err
}
data := NewSymbolResponse(symbol)
api.symbolsMu.Lock()
defer api.symbolsMu.Unlock()
api.symbols[data.Id] = symbol
return data, nil
}
func (api *API) GetSymbolAtLocation(ctx context.Context, projectId Handle[project.Project], location Handle[ast.Node]) (*SymbolResponse, error) {
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
}
api.filesMu.Lock()
defer api.filesMu.Unlock()
sourceFile, ok := api.files[fileHandle]
if !ok {
return nil, fmt.Errorf("file %q not found", fileHandle)
}
token := astnav.GetTokenAtPosition(sourceFile, pos)
if token == nil {
return nil, fmt.Errorf("token not found at position %d in file %q", pos, sourceFile.FileName())
}
node := ast.FindAncestorKind(token, kind)
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 := ls.NewLanguageService(project.GetProgram(), snapshot)
symbol := languageService.GetSymbolAtLocation(ctx, node)
if symbol == nil {
return nil, nil
}
data := NewSymbolResponse(symbol)
api.symbolsMu.Lock()
defer api.symbolsMu.Unlock()
api.symbols[data.Id] = symbol
return data, nil
}
func (api *API) GetTypeOfSymbol(ctx context.Context, projectId Handle[project.Project], symbolHandle Handle[ast.Symbol]) (*TypeResponse, error) {
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 := ls.NewLanguageService(project.GetProgram(), snapshot)
t := languageService.GetTypeOfSymbol(ctx, symbol)
if t == nil {
return nil, nil
}
return NewTypeData(t), nil
}
func (api *API) GetSourceFile(projectId Handle[project.Project], fileName string) (*ast.SourceFile, error) {
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)
}
api.filesMu.Lock()
defer api.filesMu.Unlock()
api.files[FileHandle(sourceFile)] = sourceFile
return sourceFile, nil
}
func (api *API) releaseHandle(handle string) error {
switch handle[0] {
case handlePrefixProject:
projectId := Handle[project.Project](handle)
_, ok := api.projects[projectId]
if !ok {
return fmt.Errorf("project %q not found", handle)
}
delete(api.projects, projectId)
case handlePrefixFile:
fileId := Handle[ast.SourceFile](handle)
api.filesMu.Lock()
defer api.filesMu.Unlock()
_, ok := api.files[fileId]
if !ok {
return fmt.Errorf("file %q not found", handle)
}
delete(api.files, fileId)
case handlePrefixSymbol:
symbolId := Handle[ast.Symbol](handle)
api.symbolsMu.Lock()
defer api.symbolsMu.Unlock()
_, ok := api.symbols[symbolId]
if !ok {
return fmt.Errorf("symbol %q not found", handle)
}
delete(api.symbols, symbolId)
case handlePrefixType:
typeId := Handle[checker.Type](handle)
api.typesMu.Lock()
defer api.typesMu.Unlock()
_, ok := api.types[typeId]
if !ok {
return fmt.Errorf("type %q not found", handle)
}
delete(api.types, typeId)
default:
return fmt.Errorf("unhandled handle type %q", handle[0])
}
return nil
}
func (api *API) toAbsoluteFileName(fileName string) string {
return tspath.GetNormalizedAbsolutePath(fileName, api.session.GetCurrentDirectory())
}
func (api *API) toPath(fileName string) tspath.Path {
return tspath.ToPath(fileName, api.session.GetCurrentDirectory(), api.session.FS().UseCaseSensitiveFileNames())
}
func encodeJSON(v any, err error) ([]byte, error) {
if err != nil {
return nil, err
}
return json.Marshal(v)
}