diff --git a/_packages/native-preview/src/api/async/api.ts b/_packages/native-preview/src/api/async/api.ts index 77e19bf93f9..9b1ce0f548c 100644 --- a/_packages/native-preview/src/api/async/api.ts +++ b/_packages/native-preview/src/api/async/api.ts @@ -1127,6 +1127,25 @@ export class Checker { return data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined); } + getSymbolOfSourceFile(file: DocumentIdentifier): Promise; + getSymbolOfSourceFile(files: readonly DocumentIdentifier[]): Promise<(Symbol | undefined)[]>; + async getSymbolOfSourceFile(fileOrFiles: DocumentIdentifier | readonly DocumentIdentifier[]): Promise { + if (Array.isArray(fileOrFiles)) { + const data = await this.client.apiRequest<(SymbolResponse | null)[]>("getSymbolsOfSourceFiles", { + snapshot: this.snapshotId, + project: this.project.id, + files: fileOrFiles, + }); + return data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined); + } + const data = await this.client.apiRequest("getSymbolOfSourceFile", { + snapshot: this.snapshotId, + project: this.project.id, + file: fileOrFiles as DocumentIdentifier, + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + /** * Get the type of a symbol. Always returns a type; for symbols whose type * cannot be determined the checker yields the error type (use diff --git a/_packages/native-preview/src/api/sync/api.ts b/_packages/native-preview/src/api/sync/api.ts index 447656f2a57..35b027734c4 100644 --- a/_packages/native-preview/src/api/sync/api.ts +++ b/_packages/native-preview/src/api/sync/api.ts @@ -1135,6 +1135,25 @@ export class Checker { return data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined); } + getSymbolOfSourceFile(file: DocumentIdentifier): Symbol | undefined; + getSymbolOfSourceFile(files: readonly DocumentIdentifier[]): (Symbol | undefined)[]; + getSymbolOfSourceFile(fileOrFiles: DocumentIdentifier | readonly DocumentIdentifier[]): Symbol | (Symbol | undefined)[] | undefined { + if (Array.isArray(fileOrFiles)) { + const data = this.client.apiRequest<(SymbolResponse | null)[]>("getSymbolsOfSourceFiles", { + snapshot: this.snapshotId, + project: this.project.id, + files: fileOrFiles, + }); + return data.map(d => d ? this.objectRegistry.getOrCreateSymbol(d) : undefined); + } + const data = this.client.apiRequest("getSymbolOfSourceFile", { + snapshot: this.snapshotId, + project: this.project.id, + file: fileOrFiles as DocumentIdentifier, + }); + return data ? this.objectRegistry.getOrCreateSymbol(data) : undefined; + } + /** * Get the type of a symbol. Always returns a type; for symbols whose type * cannot be determined the checker yields the error type (use diff --git a/_packages/native-preview/test/async/api.test.ts b/_packages/native-preview/test/async/api.test.ts index e084ebc2731..6c53a5a0538 100644 --- a/_packages/native-preview/test/async/api.test.ts +++ b/_packages/native-preview/test/async/api.test.ts @@ -244,6 +244,53 @@ describe("Snapshot", () => { } }); + test("getSymbolOfSourceFile", async () => { + const api = spawnAPI(); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const moduleSymbol = await project.checker.getSymbolOfSourceFile("/src/foo.ts"); + assert.ok(moduleSymbol); + const exports = await moduleSymbol.getExports(); + assert.ok(exports.has(escapeLeadingUnderscores("foo"))); + } + finally { + await api.close(); + } + }); + + test("getSymbolOfSourceFile returns undefined for a non-module file", async () => { + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/script.ts": `const x = 1;`, + }); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = await project.checker.getSymbolOfSourceFile("/src/script.ts"); + assert.equal(symbol, undefined); + } + finally { + await api.close(); + } + }); + + test("getSymbolOfSourceFile batched", async () => { + const api = spawnAPI(); + try { + const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbols = await project.checker.getSymbolOfSourceFile(["/src/index.ts", "/src/foo.ts"]); + assert.equal(symbols.length, 2); + assert.ok(symbols[0]); + assert.ok(symbols[1]); + assert.strictEqual(symbols[1], await project.checker.getSymbolOfSourceFile("/src/foo.ts")); + } + finally { + await api.close(); + } + }); + test("getTypeOfSymbol", async () => { const api = spawnAPI(); try { diff --git a/_packages/native-preview/test/sync/api.test.ts b/_packages/native-preview/test/sync/api.test.ts index 0203aa28ffe..01ac90c19e4 100644 --- a/_packages/native-preview/test/sync/api.test.ts +++ b/_packages/native-preview/test/sync/api.test.ts @@ -252,6 +252,53 @@ describe("Snapshot", () => { } }); + test("getSymbolOfSourceFile", () => { + const api = spawnAPI(); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const moduleSymbol = project.checker.getSymbolOfSourceFile("/src/foo.ts"); + assert.ok(moduleSymbol); + const exports = moduleSymbol.getExports(); + assert.ok(exports.has(escapeLeadingUnderscores("foo"))); + } + finally { + api.close(); + } + }); + + test("getSymbolOfSourceFile returns undefined for a non-module file", () => { + const api = spawnAPI({ + "/tsconfig.json": "{}", + "/src/script.ts": `const x = 1;`, + }); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbol = project.checker.getSymbolOfSourceFile("/src/script.ts"); + assert.equal(symbol, undefined); + } + finally { + api.close(); + } + }); + + test("getSymbolOfSourceFile batched", () => { + const api = spawnAPI(); + try { + const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" }); + const project = snapshot.getProject("/tsconfig.json")!; + const symbols = project.checker.getSymbolOfSourceFile(["/src/index.ts", "/src/foo.ts"]); + assert.equal(symbols.length, 2); + assert.ok(symbols[0]); + assert.ok(symbols[1]); + assert.strictEqual(symbols[1], project.checker.getSymbolOfSourceFile("/src/foo.ts")); + } + finally { + api.close(); + } + }); + test("getTypeOfSymbol", () => { const api = spawnAPI(); try { diff --git a/internal/api/proto.go b/internal/api/proto.go index fd064deec70..201e05333d9 100644 --- a/internal/api/proto.go +++ b/internal/api/proto.go @@ -76,6 +76,8 @@ const ( MethodGetDefaultProjectForFile Method = "getDefaultProjectForFile" MethodGetSymbolAtPosition Method = "getSymbolAtPosition" MethodGetSymbolsAtPositions Method = "getSymbolsAtPositions" + MethodGetSymbolOfSourceFile Method = "getSymbolOfSourceFile" + MethodGetSymbolsOfSourceFiles Method = "getSymbolsOfSourceFiles" MethodGetSymbolAtLocation Method = "getSymbolAtLocation" MethodGetSymbolsAtLocations Method = "getSymbolsAtLocations" MethodGetTypeOfSymbol Method = "getTypeOfSymbol" @@ -400,6 +402,8 @@ var unmarshalers = map[Method]func([]byte) (any, error){ MethodGetSymbolsAtPositions: unmarshallerFor[GetSymbolsAtPositionsParams], MethodGetSymbolAtLocation: unmarshallerFor[GetSymbolAtLocationParams], MethodGetSymbolsAtLocations: unmarshallerFor[GetSymbolsAtLocationsParams], + MethodGetSymbolOfSourceFile: unmarshallerFor[GetSymbolOfSourceFileParams], + MethodGetSymbolsOfSourceFiles: unmarshallerFor[GetSymbolsOfSourceFilesParams], MethodGetTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], MethodGetTypesOfSymbols: unmarshallerFor[GetTypesOfSymbolsParams], MethodGetDeclaredTypeOfSymbol: unmarshallerFor[GetTypeOfSymbolParams], @@ -602,6 +606,18 @@ type GetSymbolsAtPositionsParams struct { Positions []uint32 `json:"positions"` } +type GetSymbolOfSourceFileParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + File DocumentIdentifier `json:"file"` +} + +type GetSymbolsOfSourceFilesParams struct { + Snapshot SnapshotID `json:"snapshot"` + Project ProjectID `json:"project"` + Files []DocumentIdentifier `json:"files"` +} + type GetSymbolAtLocationParams struct { Snapshot SnapshotID `json:"snapshot"` Project ProjectID `json:"project"` diff --git a/internal/api/session.go b/internal/api/session.go index 21060a91cbe..b9711da3846 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -609,6 +609,10 @@ func (s *Session) HandleRequest(ctx context.Context, method string, params json. return s.handleGetSymbolAtLocation(ctx, parsed.(*GetSymbolAtLocationParams)) case string(MethodGetSymbolsAtLocations): return s.handleGetSymbolsAtLocations(ctx, parsed.(*GetSymbolsAtLocationsParams)) + case string(MethodGetSymbolOfSourceFile): + return s.handleGetSymbolOfSourceFile(ctx, parsed.(*GetSymbolOfSourceFileParams)) + case string(MethodGetSymbolsOfSourceFiles): + return s.handleGetSymbolsOfSourceFiles(ctx, parsed.(*GetSymbolsOfSourceFilesParams)) case string(MethodGetTypeOfSymbol): return s.handleGetTypeOfSymbol(ctx, parsed.(*GetTypeOfSymbolParams)) case string(MethodGetTypesOfSymbols): @@ -1314,6 +1318,49 @@ func (s *Session) handleGetSymbolAtPosition(ctx context.Context, params *GetSymb return setup.newSymbolResponse(symbol), nil } +// handleGetSymbolOfSourceFile returns the module symbol for a source file, if any. +// For non-module (script) files, returns nil. +func (s *Session) handleGetSymbolOfSourceFile(ctx context.Context, params *GetSymbolOfSourceFileParams) (*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + sourceFile := setup.program.GetSourceFile(params.File.ToFileName()) + if sourceFile == nil { + return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, params.File) + } + + symbol := setup.checker.GetSymbolAtLocation(sourceFile.AsNode()) + if symbol == nil { + return nil, nil + } + return setup.newSymbolResponse(symbol), nil +} + +// handleGetSymbolsOfSourceFiles returns the module symbols for multiple source files. +func (s *Session) handleGetSymbolsOfSourceFiles(ctx context.Context, params *GetSymbolsOfSourceFilesParams) ([]*SymbolResponse, error) { + setup, err := s.setupChecker(ctx, params.Snapshot, params.Project) + if err != nil { + return nil, err + } + defer setup.done() + + results := make([]*SymbolResponse, len(params.Files)) + for i, file := range params.Files { + sourceFile := setup.program.GetSourceFile(file.ToFileName()) + if sourceFile == nil { + return nil, fmt.Errorf("%w: source file not found: %v", ErrClientError, file) + } + symbol := setup.checker.GetSymbolAtLocation(sourceFile.AsNode()) + if symbol != nil { + results[i] = setup.newSymbolResponse(symbol) + } + } + return results, nil +} + // handleGetSymbolsAtPositions returns symbols at multiple positions in a file. func (s *Session) handleGetSymbolsAtPositions(ctx context.Context, params *GetSymbolsAtPositionsParams) ([]*SymbolResponse, error) { setup, err := s.setupChecker(ctx, params.Snapshot, params.Project)