diff --git a/src/ulsp/controller/scip/scip.go b/src/ulsp/controller/scip/scip.go index bd62b8a..7ce86a0 100644 --- a/src/ulsp/controller/scip/scip.go +++ b/src/ulsp/controller/scip/scip.go @@ -640,8 +640,49 @@ func (c *controller) gotoTypeDefinition(ctx context.Context, params *protocol.Ty } func (c *controller) gotoImplementation(ctx context.Context, params *protocol.ImplementationParams, result *[]protocol.LocationLink) error { - // TODO: Implement code navigation - // https://t3.uberinternal.com/browse/IDE-642 + sesh, err := c.sessions.GetFromContext(ctx) + if err != nil { + return err + } + reg := c.registries[sesh.WorkspaceRoot] + if reg == nil { + return nil + } + + mappedPosition, err := c.getBasePosition(ctx, params.TextDocument, params.Position) + if err != nil { + return err + } else if mappedPosition == nil { + return nil + } + + baseLocations, err := reg.Implementation(params.TextDocument.URI, *mappedPosition) + if err != nil { + return fmt.Errorf("failed to get implementations: %w", err) + } + + if len(baseLocations) == 0 { + return nil + } + + var originSel *protocol.Range + if _, occ, err := reg.Hover(params.TextDocument.URI, *mappedPosition); err == nil && occ != nil { + rng := mapper.ScipToProtocolRange(occ.Range) + mapped := c.getLatestRange(ctx, params.TextDocument, rng) + originSel = &mapped + } + + for _, implLoc := range baseLocations { + l := protocol.LocationLink{TargetURI: implLoc.URI} + latestRange := c.getLatestRange(ctx, protocol.TextDocumentIdentifier{URI: implLoc.URI}, implLoc.Range) + l.TargetRange = latestRange + l.TargetSelectionRange = latestRange + if originSel != nil { + l.OriginSelectionRange = originSel + } + *result = append(*result, l) + } + return nil } diff --git a/src/ulsp/controller/scip/scip_test.go b/src/ulsp/controller/scip/scip_test.go index 0a8245b..84099c6 100644 --- a/src/ulsp/controller/scip/scip_test.go +++ b/src/ulsp/controller/scip/scip_test.go @@ -1128,10 +1128,90 @@ func TestGotoImplementation(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) - c, _ := getMockedController(t, ctrl) - err := c.gotoImplementation(ctx, nil, nil) + tests := []struct { + name string + setupMocks func(t *testing.T, c *controller, reg *MockRegistry) + expected []protocol.LocationLink + expectedErr error + }{ + { + name: "no implementations", + setupMocks: func(t *testing.T, c *controller, reg *MockRegistry) { + positionMapper := docsyncmock.NewMockPositionMapper(ctrl) + positionMapper.EXPECT().MapCurrentPositionToBase(gomock.Any()).Return(protocol.Position{Line: 1, Character: 1}, false, nil) + documents := docsyncmock.NewMockController(ctrl) + documents.EXPECT().GetPositionMapper(gomock.Any(), gomock.Any()).Return(positionMapper, nil) + c.documents = documents - assert.NoError(t, err) + reg.EXPECT().Implementation(gomock.Any(), gomock.Any()).Return([]protocol.Location{}, nil) + }, + expected: []protocol.LocationLink{}, + }, + { + name: "has error return", + setupMocks: func(t *testing.T, c *controller, reg *MockRegistry) { + positionMapper := docsyncmock.NewMockPositionMapper(ctrl) + positionMapper.EXPECT().MapCurrentPositionToBase(gomock.Any()).Return(protocol.Position{Line: 1, Character: 1}, false, nil) + documents := docsyncmock.NewMockController(ctrl) + documents.EXPECT().GetPositionMapper(gomock.Any(), gomock.Any()).Return(positionMapper, nil) + c.documents = documents + + reg.EXPECT().Implementation(gomock.Any(), gomock.Any()).Return(nil, errors.New("test error")) + }, + expected: []protocol.LocationLink{}, + expectedErr: errors.New("test error"), + }, + { + name: "normal return with origin selection", + setupMocks: func(t *testing.T, c *controller, reg *MockRegistry) { + positionMapper := docsyncmock.NewMockPositionMapper(ctrl) + positionMapper.EXPECT().MapCurrentPositionToBase(gomock.Any()).Return(protocol.Position{Line: 2, Character: 2}, false, nil) + + positionMapper.EXPECT().MapBasePositionToCurrent(gomock.Any()).DoAndReturn(func(pos protocol.Position) (protocol.Position, error) { return pos, nil }).AnyTimes() + documents := docsyncmock.NewMockController(ctrl) + documents.EXPECT().GetPositionMapper(gomock.Any(), gomock.Any()).Return(positionMapper, nil).AnyTimes() + c.documents = documents + + reg.EXPECT().Implementation(gomock.Any(), gomock.Any()).Return([]protocol.Location{ + { + URI: uri.URI("file:///impl.go"), + Range: protocol.Range{ + Start: protocol.Position{Line: 10, Character: 3}, + End: protocol.Position{Line: 10, Character: 9}, + }, + }, + }, nil) + + reg.EXPECT().Hover(gomock.Any(), gomock.Any()).Return("", &model.Occurrence{Range: []int32{2, 2, 3}}, nil) + }, + expected: []protocol.LocationLink{ + { + TargetURI: uri.URI("file:///impl.go"), + TargetRange: protocol.Range{Start: protocol.Position{Line: 10, Character: 3}, End: protocol.Position{Line: 10, Character: 9}}, + TargetSelectionRange: protocol.Range{Start: protocol.Position{Line: 10, Character: 3}, End: protocol.Position{Line: 10, Character: 9}}, + OriginSelectionRange: &protocol.Range{Start: protocol.Position{Line: 2, Character: 2}, End: protocol.Position{Line: 2, Character: 3}}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, reg := getMockedController(t, ctrl) + tt.setupMocks(t, &c, reg) + + req := &protocol.ImplementationParams{TextDocumentPositionParams: getMockTextDocumentPositionParams()} + res := []protocol.LocationLink{} + err := c.gotoImplementation(ctx, req, &res) + + if tt.expectedErr != nil { + assert.ErrorContains(t, err, tt.expectedErr.Error()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, res) + } + }) + } } func TestReferences(t *testing.T) { diff --git a/src/ulsp/controller/scip/types.go b/src/ulsp/controller/scip/types.go index fbb1816..dce0c26 100644 --- a/src/ulsp/controller/scip/types.go +++ b/src/ulsp/controller/scip/types.go @@ -43,6 +43,8 @@ type Registry interface { Definition(uri uri.URI, loc protocol.Position) (*model.SymbolOccurrence, *model.SymbolOccurrence, error) // References returns the locations a symbol is referenced at in the entire index References(uri uri.URI, loc protocol.Position) ([]protocol.Location, error) + // Implementation returns the locations where a symbol is implemented + Implementation(uri uri.URI, loc protocol.Position) ([]protocol.Location, error) // Hover returns the hover information for a given position, as well as it's occurrence Hover(uri uri.URI, loc protocol.Position) (string, *model.Occurrence, error) // DocumentSymbols returns the document symbols for a given document