@@ -14,6 +14,7 @@ use pyrefly_build::handle::Handle;
1414use pyrefly_python:: docstring:: Docstring ;
1515use pyrefly_python:: ignore:: Ignore ;
1616use pyrefly_python:: ignore:: find_comment_start_in_line;
17+ #[ cfg( test) ]
1718use pyrefly_python:: module_name:: ModuleName ;
1819use pyrefly_python:: symbol_kind:: SymbolKind ;
1920use pyrefly_types:: callable:: Callable ;
@@ -27,6 +28,7 @@ use pyrefly_types::types::Forallable;
2728use pyrefly_types:: types:: Type ;
2829use pyrefly_util:: lined_buffer:: LineNumber ;
2930use ruff_python_ast:: name:: Name ;
31+ use ruff_text_size:: TextRange ;
3032use ruff_text_size:: TextSize ;
3133
3234use crate :: alt:: answers_solver:: AnswersSolver ;
@@ -172,6 +174,9 @@ impl HoverValue {
172174 . display
173175 . clone ( )
174176 . unwrap_or_else ( || self . type_ . as_hover_string ( ) ) ;
177+ // Ensure callable hover bodies always contain a proper `def name(...)` so IDE syntax
178+ // highlighting stays consistent, even when metadata is missing and we fall back to
179+ // inferred identifiers.
175180 let snippet = format_hover_code_snippet ( & self . type_ , self . name . as_deref ( ) , type_display) ;
176181 let kind_formatted = self . kind . map_or_else (
177182 || {
@@ -186,18 +191,17 @@ impl HoverValue {
186191 . name
187192 . as_ref ( )
188193 . map_or ( "" . to_owned ( ) , |s| format ! ( "{s}: " ) ) ;
189- let heading = if let Some ( callable_heading) = snippet. heading . as_ref ( ) {
190- format ! ( "{}{}\n " , kind_formatted, callable_heading)
191- } else {
192- format ! ( "{}{}" , kind_formatted, name_formatted)
193- } ;
194194
195195 Hover {
196196 contents : HoverContents :: Markup ( MarkupContent {
197197 kind : MarkupKind :: Markdown ,
198198 value : format ! (
199- "```python\n {}{}\n ```{}{}" ,
200- heading, snippet. body, docstring_formatted, symbol_def_formatted
199+ "```python\n {}{}{}\n ```{}{}" ,
200+ kind_formatted,
201+ name_formatted,
202+ snippet. body,
203+ docstring_formatted,
204+ symbol_def_formatted
201205 ) ,
202206 } ) ,
203207 range : None ,
@@ -249,30 +253,72 @@ fn expand_callable_kwargs_for_hover<'a>(
249253 }
250254}
251255
252- fn fallback_hover_name_from_type ( type_ : & Type , current_module : ModuleName ) -> Option < String > {
256+ /// If we can't determine a symbol name via go-to-definition, fall back to what the
257+ /// type metadata knows about the callable. This primarily handles third-party stubs
258+ /// where we only have typeshed information.
259+ fn fallback_hover_name_from_type ( type_ : & Type ) -> Option < String > {
253260 match type_ {
254- Type :: Function ( function) => Some ( function. metadata . kind . format ( current_module) ) ,
261+ Type :: Function ( function) => Some (
262+ function
263+ . metadata
264+ . kind
265+ . function_name ( )
266+ . into_owned ( )
267+ . to_string ( ) ,
268+ ) ,
255269 Type :: BoundMethod ( bound_method) => match & bound_method. func {
256- BoundMethodType :: Function ( function) => {
257- Some ( function. metadata . kind . format ( current_module) )
258- }
259- BoundMethodType :: Forall ( forall) => {
260- Some ( forall. body . metadata . kind . format ( current_module) )
261- }
262- BoundMethodType :: Overload ( overload) => {
263- Some ( overload. metadata . kind . format ( current_module) )
264- }
270+ BoundMethodType :: Function ( function) => Some (
271+ function
272+ . metadata
273+ . kind
274+ . function_name ( )
275+ . into_owned ( )
276+ . to_string ( ) ,
277+ ) ,
278+ BoundMethodType :: Forall ( forall) => Some (
279+ forall
280+ . body
281+ . metadata
282+ . kind
283+ . function_name ( )
284+ . into_owned ( )
285+ . to_string ( ) ,
286+ ) ,
287+ BoundMethodType :: Overload ( overload) => Some (
288+ overload
289+ . metadata
290+ . kind
291+ . function_name ( )
292+ . into_owned ( )
293+ . to_string ( ) ,
294+ ) ,
265295 } ,
266- Type :: Overload ( overload) => Some ( overload. metadata . kind . format ( current_module) ) ,
296+ Type :: Overload ( overload) => Some (
297+ overload
298+ . metadata
299+ . kind
300+ . function_name ( )
301+ . into_owned ( )
302+ . to_string ( ) ,
303+ ) ,
267304 Type :: Forall ( forall) => match & forall. body {
268- Forallable :: Function ( function) => Some ( function. metadata . kind . format ( current_module) ) ,
305+ Forallable :: Function ( function) => Some (
306+ function
307+ . metadata
308+ . kind
309+ . function_name ( )
310+ . into_owned ( )
311+ . to_string ( ) ,
312+ ) ,
269313 Forallable :: Callable ( _) | Forallable :: TypeAlias ( _) => None ,
270314 } ,
271- Type :: Type ( inner) => fallback_hover_name_from_type ( inner, current_module ) ,
315+ Type :: Type ( inner) => fallback_hover_name_from_type ( inner) ,
272316 _ => None ,
273317 }
274318}
275319
320+ /// Extract the identifier under the cursor directly from the file contents so we can
321+ /// label hover results even when go-to-definition fails.
276322fn identifier_text_at (
277323 transaction : & Transaction < ' _ > ,
278324 handle : & Handle ,
@@ -282,10 +328,7 @@ fn identifier_text_at(
282328 let contents = module. contents ( ) ;
283329 let bytes = contents. as_bytes ( ) ;
284330 let len = bytes. len ( ) ;
285- let mut pos = position. to_usize ( ) ;
286- if pos > len {
287- pos = len;
288- }
331+ let pos = position. to_usize ( ) . min ( len) ;
289332 let is_ident_char = |b : u8 | b == b'_' || b. is_ascii_alphanumeric ( ) ;
290333 let mut start = pos;
291334 while start > 0 && is_ident_char ( bytes[ start - 1 ] ) {
@@ -296,10 +339,10 @@ fn identifier_text_at(
296339 end += 1 ;
297340 }
298341 if start == end {
299- None
300- } else {
301- Some ( contents[ start..end] . to_string ( ) )
342+ return None ;
302343 }
344+ let range = TextRange :: new ( TextSize :: new ( start as u32 ) , TextSize :: new ( end as u32 ) ) ;
345+ Some ( module. code_at ( range) . to_owned ( ) )
303346}
304347
305348pub fn get_hover (
@@ -340,8 +383,7 @@ pub fn get_hover(
340383
341384 // Otherwise, fall through to the existing type hover logic
342385 let type_ = transaction. get_type_at ( handle, position) ?;
343- let current_module = handle. module ( ) ;
344- let fallback_name_from_type = fallback_hover_name_from_type ( & type_, current_module) ;
386+ let fallback_name_from_type = fallback_hover_name_from_type ( & type_) ;
345387 let type_display = transaction. ad_hoc_solve ( handle, {
346388 let mut cloned = type_. clone ( ) ;
347389 move |solver| {
@@ -351,7 +393,7 @@ pub fn get_hover(
351393 cloned. as_hover_string ( )
352394 }
353395 } ) ;
354- let ( kind, mut name, docstring_range, module) = if let Some ( FindDefinitionItemWithDocstring {
396+ let ( kind, name, docstring_range, module) = if let Some ( FindDefinitionItemWithDocstring {
355397 metadata,
356398 definition_range : definition_location,
357399 module,
@@ -389,9 +431,7 @@ pub fn get_hover(
389431 ( None , fallback_name_from_type, None , None )
390432 } ;
391433
392- if name. is_none ( ) {
393- name = identifier_text_at ( transaction, handle, position) ;
394- }
434+ let name = name. or_else ( || identifier_text_at ( transaction, handle, position) ) ;
395435
396436 let docstring = if let ( Some ( docstring) , Some ( module) ) = ( docstring_range, module) {
397437 Some ( Docstring ( docstring, module) )
@@ -452,14 +492,14 @@ mod tests {
452492 #[ test]
453493 fn fallback_uses_function_metadata ( ) {
454494 let ty = make_function_type ( "numpy" , "arange" ) ;
455- let fallback = fallback_hover_name_from_type ( & ty, ModuleName :: from_str ( "user_code" ) ) ;
456- assert_eq ! ( fallback. as_deref( ) , Some ( "numpy. arange" ) ) ;
495+ let fallback = fallback_hover_name_from_type ( & ty) ;
496+ assert_eq ! ( fallback. as_deref( ) , Some ( "arange" ) ) ;
457497 }
458498
459499 #[ test]
460500 fn fallback_recurses_through_type_wrapper ( ) {
461501 let ty = Type :: Type ( Box :: new ( make_function_type ( "pkg.subpkg" , "run" ) ) ) ;
462- let fallback = fallback_hover_name_from_type ( & ty, ModuleName :: from_str ( "other" ) ) ;
463- assert_eq ! ( fallback. as_deref( ) , Some ( "pkg.subpkg. run" ) ) ;
502+ let fallback = fallback_hover_name_from_type ( & ty) ;
503+ assert_eq ! ( fallback. as_deref( ) , Some ( "run" ) ) ;
464504 }
465505}
0 commit comments