@@ -18,6 +18,7 @@ use pyrefly_python::docstring::parse_parameter_documentation;
1818use pyrefly_python:: ignore:: Ignore ;
1919use pyrefly_python:: ignore:: Tool ;
2020use pyrefly_python:: ignore:: find_comment_start_in_line;
21+ #[ cfg( test) ]
2122use pyrefly_python:: module_name:: ModuleName ;
2223use pyrefly_python:: symbol_kind:: SymbolKind ;
2324use pyrefly_types:: callable:: Callable ;
@@ -234,6 +235,9 @@ impl HoverValue {
234235 . display
235236 . clone ( )
236237 . unwrap_or_else ( || self . type_ . as_hover_string ( ) ) ;
238+ // Ensure callable hover bodies always contain a proper `def name(...)` so IDE syntax
239+ // highlighting stays consistent, even when metadata is missing and we fall back to
240+ // inferred identifiers.
237241 let snippet = format_hover_code_snippet ( & self . type_ , self . name . as_deref ( ) , type_display) ;
238242 let kind_formatted = self . kind . map_or_else (
239243 || {
@@ -248,11 +252,6 @@ impl HoverValue {
248252 . name
249253 . as_ref ( )
250254 . map_or ( "" . to_owned ( ) , |s| format ! ( "{s}: " ) ) ;
251- let heading = if let Some ( callable_heading) = snippet. heading . as_ref ( ) {
252- format ! ( "{}{}\n " , kind_formatted, callable_heading)
253- } else {
254- format ! ( "{}{}" , kind_formatted, name_formatted)
255- } ;
256255
257256 Hover {
258257 contents : HoverContents :: Markup ( MarkupContent {
@@ -316,30 +315,72 @@ fn expand_callable_kwargs_for_hover<'a>(
316315 }
317316}
318317
319- fn fallback_hover_name_from_type ( type_ : & Type , current_module : ModuleName ) -> Option < String > {
318+ /// If we can't determine a symbol name via go-to-definition, fall back to what the
319+ /// type metadata knows about the callable. This primarily handles third-party stubs
320+ /// where we only have typeshed information.
321+ fn fallback_hover_name_from_type ( type_ : & Type ) -> Option < String > {
320322 match type_ {
321- Type :: Function ( function) => Some ( function. metadata . kind . format ( current_module) ) ,
323+ Type :: Function ( function) => Some (
324+ function
325+ . metadata
326+ . kind
327+ . function_name ( )
328+ . into_owned ( )
329+ . to_string ( ) ,
330+ ) ,
322331 Type :: BoundMethod ( bound_method) => match & bound_method. func {
323- BoundMethodType :: Function ( function) => {
324- Some ( function. metadata . kind . format ( current_module) )
325- }
326- BoundMethodType :: Forall ( forall) => {
327- Some ( forall. body . metadata . kind . format ( current_module) )
328- }
329- BoundMethodType :: Overload ( overload) => {
330- Some ( overload. metadata . kind . format ( current_module) )
331- }
332+ BoundMethodType :: Function ( function) => Some (
333+ function
334+ . metadata
335+ . kind
336+ . function_name ( )
337+ . into_owned ( )
338+ . to_string ( ) ,
339+ ) ,
340+ BoundMethodType :: Forall ( forall) => Some (
341+ forall
342+ . body
343+ . metadata
344+ . kind
345+ . function_name ( )
346+ . into_owned ( )
347+ . to_string ( ) ,
348+ ) ,
349+ BoundMethodType :: Overload ( overload) => Some (
350+ overload
351+ . metadata
352+ . kind
353+ . function_name ( )
354+ . into_owned ( )
355+ . to_string ( ) ,
356+ ) ,
332357 } ,
333- Type :: Overload ( overload) => Some ( overload. metadata . kind . format ( current_module) ) ,
358+ Type :: Overload ( overload) => Some (
359+ overload
360+ . metadata
361+ . kind
362+ . function_name ( )
363+ . into_owned ( )
364+ . to_string ( ) ,
365+ ) ,
334366 Type :: Forall ( forall) => match & forall. body {
335- Forallable :: Function ( function) => Some ( function. metadata . kind . format ( current_module) ) ,
367+ Forallable :: Function ( function) => Some (
368+ function
369+ . metadata
370+ . kind
371+ . function_name ( )
372+ . into_owned ( )
373+ . to_string ( ) ,
374+ ) ,
336375 Forallable :: Callable ( _) | Forallable :: TypeAlias ( _) => None ,
337376 } ,
338- Type :: Type ( inner) => fallback_hover_name_from_type ( inner, current_module ) ,
377+ Type :: Type ( inner) => fallback_hover_name_from_type ( inner) ,
339378 _ => None ,
340379 }
341380}
342381
382+ /// Extract the identifier under the cursor directly from the file contents so we can
383+ /// label hover results even when go-to-definition fails.
343384fn identifier_text_at (
344385 transaction : & Transaction < ' _ > ,
345386 handle : & Handle ,
@@ -349,10 +390,7 @@ fn identifier_text_at(
349390 let contents = module. contents ( ) ;
350391 let bytes = contents. as_bytes ( ) ;
351392 let len = bytes. len ( ) ;
352- let mut pos = position. to_usize ( ) ;
353- if pos > len {
354- pos = len;
355- }
393+ let pos = position. to_usize ( ) . min ( len) ;
356394 let is_ident_char = |b : u8 | b == b'_' || b. is_ascii_alphanumeric ( ) ;
357395 let mut start = pos;
358396 while start > 0 && is_ident_char ( bytes[ start - 1 ] ) {
@@ -363,10 +401,10 @@ fn identifier_text_at(
363401 end += 1 ;
364402 }
365403 if start == end {
366- None
367- } else {
368- Some ( contents[ start..end] . to_string ( ) )
404+ return None ;
369405 }
406+ let range = TextRange :: new ( TextSize :: new ( start as u32 ) , TextSize :: new ( end as u32 ) ) ;
407+ Some ( module. code_at ( range) . to_owned ( ) )
370408}
371409
372410pub fn get_hover (
@@ -411,8 +449,7 @@ pub fn get_hover(
411449
412450 // Otherwise, fall through to the existing type hover logic
413451 let type_ = transaction. get_type_at ( handle, position) ?;
414- let current_module = handle. module ( ) ;
415- let fallback_name_from_type = fallback_hover_name_from_type ( & type_, current_module) ;
452+ let fallback_name_from_type = fallback_hover_name_from_type ( & type_) ;
416453 let type_display = transaction. ad_hoc_solve ( handle, {
417454 let mut cloned = type_. clone ( ) ;
418455 move |solver| {
@@ -422,7 +459,7 @@ pub fn get_hover(
422459 cloned. as_hover_string ( )
423460 }
424461 } ) ;
425- let ( kind, mut name, docstring_range, module) = if let Some ( FindDefinitionItemWithDocstring {
462+ let ( kind, name, docstring_range, module) = if let Some ( FindDefinitionItemWithDocstring {
426463 metadata,
427464 definition_range : definition_location,
428465 module,
@@ -460,9 +497,7 @@ pub fn get_hover(
460497 ( None , fallback_name_from_type, None , None )
461498 } ;
462499
463- if name. is_none ( ) {
464- name = identifier_text_at ( transaction, handle, position) ;
465- }
500+ let name = name. or_else ( || identifier_text_at ( transaction, handle, position) ) ;
466501
467502 let docstring = if let ( Some ( docstring) , Some ( module) ) = ( docstring_range, module) {
468503 Some ( Docstring ( docstring, module) )
@@ -620,14 +655,14 @@ mod tests {
620655 #[ test]
621656 fn fallback_uses_function_metadata ( ) {
622657 let ty = make_function_type ( "numpy" , "arange" ) ;
623- let fallback = fallback_hover_name_from_type ( & ty, ModuleName :: from_str ( "user_code" ) ) ;
624- assert_eq ! ( fallback. as_deref( ) , Some ( "numpy. arange" ) ) ;
658+ let fallback = fallback_hover_name_from_type ( & ty) ;
659+ assert_eq ! ( fallback. as_deref( ) , Some ( "arange" ) ) ;
625660 }
626661
627662 #[ test]
628663 fn fallback_recurses_through_type_wrapper ( ) {
629664 let ty = Type :: Type ( Box :: new ( make_function_type ( "pkg.subpkg" , "run" ) ) ) ;
630- let fallback = fallback_hover_name_from_type ( & ty, ModuleName :: from_str ( "other" ) ) ;
631- assert_eq ! ( fallback. as_deref( ) , Some ( "pkg.subpkg. run" ) ) ;
665+ let fallback = fallback_hover_name_from_type ( & ty) ;
666+ assert_eq ! ( fallback. as_deref( ) , Some ( "run" ) ) ;
632667 }
633668}
0 commit comments