Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ require (
github.com/Microsoft/go-winio v0.6.2
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68
github.com/google/go-cmp v0.7.0
github.com/mackerelio/go-osstat v0.2.8
github.com/mackerelio/go-osstat v0.2.7
github.com/peter-evans/patience v0.3.0
github.com/zeebo/xxh3 v1.1.0
golang.org/x/sync v0.22.0
golang.org/x/sys v0.47.0
golang.org/x/text v0.40.0
golang.org/x/sync v0.21.0
golang.org/x/sys v0.46.0
golang.org/x/text v0.38.0
gotest.tools/v3 v3.5.2
)

Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/mackerelio/go-osstat v0.2.8 h1:I2duicTaCGWoM53XwAwA9OIe1inu0xnVs8/pqOWWVr4=
github.com/mackerelio/go-osstat v0.2.8/go.mod h1:SyS3XxKdoSKJnTGTkN5Yrh6VUQVuAURACfE6y+2DN4k=
github.com/mackerelio/go-osstat v0.2.7 h1:TCavZi10wF49bT6iQZ9eT2keGZQpC69MTDfdJej5e94=
github.com/mackerelio/go-osstat v0.2.7/go.mod h1:dwpYh5pIPmvk+IEwBKNIWRFMB92mrC08CmXOhDC7nQk=
github.com/peter-evans/patience v0.3.0 h1:rX0JdJeepqdQl1Sk9c9uvorjYYzL2TfgLX1adqYm9cA=
github.com/peter-evans/patience v0.3.0/go.mod h1:Kmxu5sY1NmBLFSStvXjX1wS9mIv7wMcP/ubucyMOAu0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
27 changes: 27 additions & 0 deletions pkg/api/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ const (
MethodGetConstraintOfTypeParameter Method = "getConstraintOfTypeParameter"
MethodGetBaseConstraintOfType Method = "getBaseConstraintOfType"
MethodGetTypeArguments Method = "getTypeArguments"
MethodGetImportAdderEdits Method = "getImportAdderEdits"
MethodGetTrueTypeOfConditionalType Method = "getTrueTypeOfConditionalType"
MethodGetFalseTypeOfConditionalType Method = "getFalseTypeOfConditionalType"
MethodGetConstantValue Method = "getConstantValue"
Expand Down Expand Up @@ -438,6 +439,7 @@ var unmarshalers = map[Method]func([]byte) (any, error){
MethodGetConstraintOfTypeParameter: unmarshallerFor[CheckerTypeParams],
MethodGetBaseConstraintOfType: unmarshallerFor[CheckerTypeParams],
MethodGetTypeArguments: unmarshallerFor[CheckerTypeParams],
MethodGetImportAdderEdits: unmarshallerFor[GetImportAdderEditsParams],
MethodGetConstantValue: unmarshallerFor[CheckerNodeParams],
MethodGetSignatureFromDeclaration: unmarshallerFor[CheckerNodeParams],
MethodGetExportSpecifierLocalTarget: unmarshallerFor[CheckerNodeParams],
Expand Down Expand Up @@ -1014,6 +1016,31 @@ type GetTypesAtPositionsParams struct {
Positions []uint32 `json:"positions"`
}

type ImportAdderActionKind string

const (
ImportAdderActionKindImportSymbol ImportAdderActionKind = "importSymbol"
)

type ImportAdderAction struct {
Kind ImportAdderActionKind `json:"kind"`
Symbol SymbolID `json:"symbol,omitempty"`
IsValidTypeOnlyUseSite *bool `json:"isValidTypeOnlyUseSite,omitempty"`
}

type GetImportAdderEditsParams struct {
Snapshot SnapshotID `json:"snapshot"`
Project ProjectID `json:"project"`
File DocumentIdentifier `json:"file"`
Actions []ImportAdderAction `json:"actions"`
}

type TextEdit struct {
Pos int `json:"pos"`
End int `json:"end"`
NewText string `json:"newText"`
}

// TypeToTypeNodeParams are the parameters for the typeToTypeNode method.
type TypeToTypeNodeParams struct {
Snapshot SnapshotID `json:"snapshot"`
Expand Down
110 changes: 110 additions & 0 deletions pkg/api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/buke/typescript-go-internal/pkg/core"
"github.com/buke/typescript-go-internal/pkg/json"
"github.com/buke/typescript-go-internal/pkg/ls"
"github.com/buke/typescript-go-internal/pkg/ls/autoimport"
"github.com/buke/typescript-go-internal/pkg/ls/lsconv"
"github.com/buke/typescript-go-internal/pkg/lsp/lsproto"
"github.com/buke/typescript-go-internal/pkg/nodebuilder"
"github.com/buke/typescript-go-internal/pkg/pprof"
Expand Down Expand Up @@ -696,6 +698,8 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json.
return s.handleGetBaseConstraintOfType(ctx, parsed.(*CheckerTypeParams))
case string(MethodGetTypeArguments):
return s.handleGetTypeArguments(ctx, parsed.(*CheckerTypeParams))
case string(MethodGetImportAdderEdits):
return s.handleGetImportAdderEdits(ctx, parsed.(*GetImportAdderEditsParams))
case string(MethodGetConstantValue):
return s.handleGetConstantValue(ctx, parsed.(*CheckerNodeParams))
case string(MethodGetSignatureFromDeclaration):
Expand Down Expand Up @@ -1559,6 +1563,112 @@ func (s *Session) handleGetTargetOfSignature(_ context.Context, params *GetSigna
return s.resolveSignaturePropertyOfSignature(params, (*checker.Signature).Target)
}

func (s *Session) handleGetImportAdderEdits(ctx context.Context, params *GetImportAdderEditsParams) ([]*TextEdit, error) {
sd, err := s.getSnapshotData(params.Snapshot)
if err != nil {
return nil, err
}

projectPath := parseProjectHandle(params.Project)
workingSnapshot := sd.snapshot
program, err := sd.getProgram(params.Project)
if err != nil {
return nil, err
}
sourceFile := program.GetSourceFile(params.File.ToFileName())
if sourceFile == nil {
return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File)
}

userPreferences := workingSnapshot.UserPreferences()
if registry := workingSnapshot.AutoImportRegistry(); registry == nil ||
!registry.IsPreparedForImportingFile(sourceFile.FileName(), projectPath, userPreferences) {
preparedSnapshot := s.projectSession.GetSnapshotWithAutoImports(ctx, workingSnapshot, params.File.ToURI(s.projectSession.GetCurrentDirectory()))
defer preparedSnapshot.Deref(s.projectSession)

workingSnapshot = preparedSnapshot
proj := workingSnapshot.ProjectCollection.GetProjectByPath(projectPath)
if proj == nil {
return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectPath)
}
program = proj.GetProgram()
if program == nil {
return nil, fmt.Errorf("%w: project has no program", ErrClientError)
}
sourceFile = program.GetSourceFile(params.File.ToFileName())
if sourceFile == nil {
return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File)
}
userPreferences = workingSnapshot.UserPreferences()
}

registry := workingSnapshot.AutoImportRegistry()
if registry == nil {
return []*TextEdit{}, nil
}

ch, done := program.GetTypeChecker(ctx)
defer done()

view := autoimport.NewView(
registry,
sourceFile,
projectPath,
program,
userPreferences.ModuleSpecifierPreferences(),
)
importAdder := autoimport.NewImportAdder(
ctx,
program,
ch,
sourceFile,
view,
workingSnapshot.GetPreferences(sourceFile.FileName()).FormatCodeSettings,
workingSnapshot.Converters(),
userPreferences,
)

for i, action := range params.Actions {
switch action.Kind {
case ImportAdderActionKindImportSymbol:
if action.Symbol == 0 {
return nil, fmt.Errorf("%w: import adder action %d missing symbol", ErrClientError, i)
}
symbol, err := sd.resolveSymbolHandle(action.Symbol)
if err != nil {
return nil, err
}
isValidTypeOnlyUseSite := true
if action.IsValidTypeOnlyUseSite != nil {
isValidTypeOnlyUseSite = *action.IsValidTypeOnlyUseSite
}
importAdder.AddImportFromExportedSymbol(symbol, isValidTypeOnlyUseSite)
default:
return nil, fmt.Errorf("%w: unknown import adder action kind %q", ErrClientError, action.Kind)
}
}

if !importAdder.HasFixes() {
return []*TextEdit{}, nil
}
return toAPITextEdits(sourceFile, workingSnapshot.Converters(), importAdder.Edits()), nil
}

func toAPITextEdits(sourceFile *ast.SourceFile, converters *lsconv.Converters, edits []*lsproto.TextEdit) []*TextEdit {
positionMap := sourceFile.GetPositionMap()
result := make([]*TextEdit, len(edits))
for i, edit := range edits {
start := converters.LineAndCharacterToPosition(sourceFile, edit.Range.Start)
end := converters.LineAndCharacterToPosition(sourceFile, edit.Range.End)
result[i] = &TextEdit{
Pos: positionMap.UTF8ToUTF16(int(start)),
End: positionMap.UTF8ToUTF16(int(end)),
NewText: edit.NewText,
}
}
return result
}

// resolveTypePropertyOfType resolves a type property of type `Type` and returns a type response.
func (s *Session) resolveTypePropertyOfType(params *GetTypePropertyParams, getter func(*checker.Type) *checker.Type) (*TypeResponse, error) {
sd, err := s.getSnapshotData(params.Snapshot)
Expand Down
38 changes: 38 additions & 0 deletions pkg/execute/tsctests/tsc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4132,6 +4132,44 @@ func TestTscProjectReferences(t *testing.T) {
cwd: "/home/src/workspaces/solution",
commandLineArgs: []string{"--p", "project"},
},
{
subScenario: "when project references have invalid fields",
files: FileMap{
"/home/src/workspaces/solution/project/index.ts": `export const x = 10;`,
"/home/src/workspaces/solution/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"noEmit": true
},
"files": ["index.ts"],
"references": [
{ "path": true },
{ "circular": true },
{ "path": "../utils", "circular": "yes" },
{ "path": "" },
{ "path": "../valid", "circular": true }
]
}`),
"/home/src/workspaces/solution/utils/index.ts": "export const y = 10;",
"/home/src/workspaces/solution/utils/index.d.ts": "export declare const y = 10;",
"/home/src/workspaces/solution/utils/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"composite": true
}
}`),
"/home/src/workspaces/solution/valid/index.ts": "export const z = 10;",
"/home/src/workspaces/solution/valid/index.d.ts": "export declare const z = 10;",
"/home/src/workspaces/solution/valid/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"composite": true
}
}`),
},
cwd: "/home/src/workspaces/solution",
commandLineArgs: []string{"--p", "project"},
},
{
subScenario: "default import interop uses referenced project settings",
files: FileMap{
Expand Down
37 changes: 37 additions & 0 deletions pkg/execute/tsctests/tscbuild_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,43 @@ func TestBuildConfigFileErrors(t *testing.T) {
},
commandLineArgs: []string{"--b"},
},
{
subScenario: "reports invalid project reference fields",
files: FileMap{
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"composite": true
},
"files": ["index.ts"],
"references": [
{ "path": true },
{ "circular": true },
{ "path": "./utils", "circular": "yes" },
{ "path": "" },
{ "path": "./valid", "circular": true }
]
}`),
"/home/src/workspaces/project/index.ts": "export const x = 10;",
"/home/src/workspaces/project/utils/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"composite": true
},
"files": ["index.ts"]
}`),
"/home/src/workspaces/project/utils/index.ts": "export const y = 10;",
"/home/src/workspaces/project/valid/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"composite": true
},
"files": ["index.ts"]
}`),
"/home/src/workspaces/project/valid/index.ts": "export const z = 10;",
},
commandLineArgs: []string{"--b", "--dry"},
},
{
subScenario: "reports syntax errors in config file",
files: FileMap{
Expand Down
38 changes: 30 additions & 8 deletions pkg/project/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,31 @@ func (s *Session) WithLanguageServiceAndSnapshot(
// The cloned snapshot will be adopted as the session's current snapshot in the background
// if other changes haven't been adopted in the meantime.
func (s *Session) GetLanguageServiceWithAutoImports(ctx context.Context, baseSnapshot *Snapshot, uri lsproto.DocumentUri) (*ls.LanguageService, error) {
newSnapshot := s.cloneWithAutoImports(ctx, baseSnapshot, uri, false /*callerRef*/)
project := newSnapshot.GetDefaultProject(uri)
if project == nil {
// Clone's initial ref (1) is released since we won't use this snapshot.
newSnapshot.Deref(s)
return nil, fmt.Errorf("no project found for URI %s", uri)
}

s.adoptSnapshotChangeInBackground(baseSnapshot, newSnapshot)

return ls.NewLanguageService(project.configFilePath, project.GetProgram(), newSnapshot, uri.FileName()), nil
}

// GetSnapshotWithAutoImports clones the given snapshot with auto-import
// preparation for the given URI, without flushing pending file changes.
// The returned snapshot is ref'd for the caller, which must call Deref when done.
// The cloned snapshot will also be adopted as the session's current snapshot in
// the background if other changes haven't been adopted in the meantime.
func (s *Session) GetSnapshotWithAutoImports(ctx context.Context, baseSnapshot *Snapshot, uri lsproto.DocumentUri) *Snapshot {
newSnapshot := s.cloneWithAutoImports(ctx, baseSnapshot, uri, true /*callerRef*/)
s.adoptSnapshotChangeInBackground(baseSnapshot, newSnapshot)
return newSnapshot
}

func (s *Session) cloneWithAutoImports(ctx context.Context, baseSnapshot *Snapshot, uri lsproto.DocumentUri, callerRef bool) *Snapshot {
change := SnapshotChange{
reason: UpdateReasonRequestedLanguageServiceWithAutoImports,
ResourceRequest: ResourceRequest{
Expand All @@ -1135,22 +1160,19 @@ func (s *Session) GetLanguageServiceWithAutoImports(ctx context.Context, baseSna
},
}
newSnapshot := baseSnapshot.Clone(ctx, change, baseSnapshot.fs.overlays, s)

project := newSnapshot.GetDefaultProject(uri)
if project == nil {
// Clone's initial ref (1) is released since we won't use this snapshot.
newSnapshot.Deref(s)
return nil, fmt.Errorf("no project found for URI %s", uri)
if callerRef {
newSnapshot.ref()
}
return newSnapshot
}

func (s *Session) adoptSnapshotChangeInBackground(baseSnapshot, newSnapshot *Snapshot) {
// The clone's initial ref (1) is transferred to adoptSnapshotChange,
// which will either promote it as the session's current snapshot or
// release it if the session has moved on.
s.backgroundQueue.Enqueue(s.backgroundCtx, func(ctx context.Context) {
s.adoptSnapshotChange(baseSnapshot, newSnapshot)
})

return ls.NewLanguageService(project.configFilePath, project.GetProgram(), newSnapshot, uri.FileName()), nil
}

// adoptSnapshotChange promotes a cloned snapshot as the session's current
Expand Down
Loading