@@ -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 ;
@@ -199,6 +200,9 @@ impl HoverValue {
199200 . display
200201 . clone ( )
201202 . unwrap_or_else ( || self . type_ . as_hover_string ( ) ) ;
203+ // Ensure callable hover bodies always contain a proper `def name(...)` so IDE syntax
204+ // highlighting stays consistent, even when metadata is missing and we fall back to
205+ // inferred identifiers.
202206 let snippet = format_hover_code_snippet ( & self . type_ , self . name . as_deref ( ) , type_display) ;
203207 let kind_formatted = self . kind . map_or_else (
204208 || {
@@ -213,11 +217,6 @@ impl HoverValue {
213217 . name
214218 . as_ref ( )
215219 . map_or ( "" . to_owned ( ) , |s| format ! ( "{s}: " ) ) ;
216- let heading = if let Some ( callable_heading) = snippet. heading . as_ref ( ) {
217- format ! ( "{}{}\n " , kind_formatted, callable_heading)
218- } else {
219- format ! ( "{}{}" , kind_formatted, name_formatted)
220- } ;
221220
222221 Hover {
223222 contents : HoverContents :: Markup ( MarkupContent {
@@ -281,30 +280,72 @@ fn expand_callable_kwargs_for_hover<'a>(
281280 }
282281}
283282
284- fn fallback_hover_name_from_type ( type_ : & Type , current_module : ModuleName ) -> Option < String > {
283+ /// If we can't determine a symbol name via go-to-definition, fall back to what the
284+ /// type metadata knows about the callable. This primarily handles third-party stubs
285+ /// where we only have typeshed information.
286+ fn fallback_hover_name_from_type ( type_ : & Type ) -> Option < String > {
285287 match type_ {
286- Type :: Function ( function) => Some ( function. metadata . kind . format ( current_module) ) ,
288+ Type :: Function ( function) => Some (
289+ function
290+ . metadata
291+ . kind
292+ . function_name ( )
293+ . into_owned ( )
294+ . to_string ( ) ,
295+ ) ,
287296 Type :: BoundMethod ( bound_method) => match & bound_method. func {
288- BoundMethodType :: Function ( function) => {
289- Some ( function. metadata . kind . format ( current_module) )
290- }
291- BoundMethodType :: Forall ( forall) => {
292- Some ( forall. body . metadata . kind . format ( current_module) )
293- }
294- BoundMethodType :: Overload ( overload) => {
295- Some ( overload. metadata . kind . format ( current_module) )
296- }
297+ BoundMethodType :: Function ( function) => Some (
298+ function
299+ . metadata
300+ . kind
301+ . function_name ( )
302+ . into_owned ( )
303+ . to_string ( ) ,
304+ ) ,
305+ BoundMethodType :: Forall ( forall) => Some (
306+ forall
307+ . body
308+ . metadata
309+ . kind
310+ . function_name ( )
311+ . into_owned ( )
312+ . to_string ( ) ,
313+ ) ,
314+ BoundMethodType :: Overload ( overload) => Some (
315+ overload
316+ . metadata
317+ . kind
318+ . function_name ( )
319+ . into_owned ( )
320+ . to_string ( ) ,
321+ ) ,
297322 } ,
298- Type :: Overload ( overload) => Some ( overload. metadata . kind . format ( current_module) ) ,
323+ Type :: Overload ( overload) => Some (
324+ overload
325+ . metadata
326+ . kind
327+ . function_name ( )
328+ . into_owned ( )
329+ . to_string ( ) ,
330+ ) ,
299331 Type :: Forall ( forall) => match & forall. body {
300- Forallable :: Function ( function) => Some ( function. metadata . kind . format ( current_module) ) ,
332+ Forallable :: Function ( function) => Some (
333+ function
334+ . metadata
335+ . kind
336+ . function_name ( )
337+ . into_owned ( )
338+ . to_string ( ) ,
339+ ) ,
301340 Forallable :: Callable ( _) | Forallable :: TypeAlias ( _) => None ,
302341 } ,
303- Type :: Type ( inner) => fallback_hover_name_from_type ( inner, current_module ) ,
342+ Type :: Type ( inner) => fallback_hover_name_from_type ( inner) ,
304343 _ => None ,
305344 }
306345}
307346
347+ /// Extract the identifier under the cursor directly from the file contents so we can
348+ /// label hover results even when go-to-definition fails.
308349fn identifier_text_at (
309350 transaction : & Transaction < ' _ > ,
310351 handle : & Handle ,
@@ -314,10 +355,7 @@ fn identifier_text_at(
314355 let contents = module. contents ( ) ;
315356 let bytes = contents. as_bytes ( ) ;
316357 let len = bytes. len ( ) ;
317- let mut pos = position. to_usize ( ) ;
318- if pos > len {
319- pos = len;
320- }
358+ let pos = position. to_usize ( ) . min ( len) ;
321359 let is_ident_char = |b : u8 | b == b'_' || b. is_ascii_alphanumeric ( ) ;
322360 let mut start = pos;
323361 while start > 0 && is_ident_char ( bytes[ start - 1 ] ) {
@@ -328,10 +366,10 @@ fn identifier_text_at(
328366 end += 1 ;
329367 }
330368 if start == end {
331- None
332- } else {
333- Some ( contents[ start..end] . to_string ( ) )
369+ return None ;
334370 }
371+ let range = TextRange :: new ( TextSize :: new ( start as u32 ) , TextSize :: new ( end as u32 ) ) ;
372+ Some ( module. code_at ( range) . to_owned ( ) )
335373}
336374
337375pub fn get_hover (
@@ -372,8 +410,7 @@ pub fn get_hover(
372410
373411 // Otherwise, fall through to the existing type hover logic
374412 let type_ = transaction. get_type_at ( handle, position) ?;
375- let current_module = handle. module ( ) ;
376- let fallback_name_from_type = fallback_hover_name_from_type ( & type_, current_module) ;
413+ let fallback_name_from_type = fallback_hover_name_from_type ( & type_) ;
377414 let type_display = transaction. ad_hoc_solve ( handle, {
378415 let mut cloned = type_. clone ( ) ;
379416 move |solver| {
@@ -383,7 +420,7 @@ pub fn get_hover(
383420 cloned. as_hover_string ( )
384421 }
385422 } ) ;
386- let ( kind, mut name, docstring_range, module) = if let Some ( FindDefinitionItemWithDocstring {
423+ let ( kind, name, docstring_range, module) = if let Some ( FindDefinitionItemWithDocstring {
387424 metadata,
388425 definition_range : definition_location,
389426 module,
@@ -421,9 +458,7 @@ pub fn get_hover(
421458 ( None , fallback_name_from_type, None , None )
422459 } ;
423460
424- if name. is_none ( ) {
425- name = identifier_text_at ( transaction, handle, position) ;
426- }
461+ let name = name. or_else ( || identifier_text_at ( transaction, handle, position) ) ;
427462
428463 let docstring = if let ( Some ( docstring) , Some ( module) ) = ( docstring_range, module) {
429464 Some ( Docstring ( docstring, module) )
@@ -581,14 +616,14 @@ mod tests {
581616 #[ test]
582617 fn fallback_uses_function_metadata ( ) {
583618 let ty = make_function_type ( "numpy" , "arange" ) ;
584- let fallback = fallback_hover_name_from_type ( & ty, ModuleName :: from_str ( "user_code" ) ) ;
585- assert_eq ! ( fallback. as_deref( ) , Some ( "numpy. arange" ) ) ;
619+ let fallback = fallback_hover_name_from_type ( & ty) ;
620+ assert_eq ! ( fallback. as_deref( ) , Some ( "arange" ) ) ;
586621 }
587622
588623 #[ test]
589624 fn fallback_recurses_through_type_wrapper ( ) {
590625 let ty = Type :: Type ( Box :: new ( make_function_type ( "pkg.subpkg" , "run" ) ) ) ;
591- let fallback = fallback_hover_name_from_type ( & ty, ModuleName :: from_str ( "other" ) ) ;
592- assert_eq ! ( fallback. as_deref( ) , Some ( "pkg.subpkg. run" ) ) ;
626+ let fallback = fallback_hover_name_from_type ( & ty) ;
627+ assert_eq ! ( fallback. as_deref( ) , Some ( "run" ) ) ;
593628 }
594629}
0 commit comments