Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion crates/pyrefly_types/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,9 +780,22 @@ impl Display for Type {

impl Type {
pub fn as_hover_string(&self) -> String {
self.as_hover_string_with_fallback_name(None)
}

pub fn as_hover_string_with_fallback_name(&self, fallback_name: Option<&str>) -> String {
let mut c = TypeDisplayContext::new(&[self]);
c.set_display_mode_to_hover();
c.display(self).to_string()
let rendered = c.display(self).to_string();
if let Some(name) = fallback_name
&& self.is_function_type()
{
let trimmed = rendered.trim_start();
if trimmed.starts_with('(') {
return format!("def {}{}: ...", name, trimmed);
}
}
rendered
}

pub fn get_types_with_locations(&self) -> Vec<(String, Option<TextRangeWithModule>)> {
Expand Down
2 changes: 2 additions & 0 deletions pyrefly/lib/lsp/non_wasm/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2257,6 +2257,7 @@ impl Server {
definition_range,
module,
docstring_range: _,
..
} = definition;
// find_global_implementations_from_definition returns Vec<TextRangeWithModule>
// but we need to return Vec<(ModuleInfo, Vec<TextRange>)> to match the helper's
Expand Down Expand Up @@ -2508,6 +2509,7 @@ impl Server {
definition_range,
module,
docstring_range: _,
..
} = definition;
transaction.find_global_references_from_definition(
handle.sys_info(),
Expand Down
211 changes: 188 additions & 23 deletions pyrefly/lib/lsp/wasm/hover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ use pyrefly_python::docstring::parse_parameter_documentation;
use pyrefly_python::ignore::Ignore;
use pyrefly_python::ignore::Tool;
use pyrefly_python::ignore::find_comment_start_in_line;
#[cfg(test)]
use pyrefly_python::module_name::ModuleName;
use pyrefly_python::symbol_kind::SymbolKind;
use pyrefly_types::callable::Callable;
use pyrefly_types::callable::Param;
use pyrefly_types::callable::ParamList;
use pyrefly_types::callable::Params;
use pyrefly_types::callable::Required;
use pyrefly_types::types::BoundMethodType;
use pyrefly_types::types::Forallable;
use pyrefly_types::types::Type;
use pyrefly_util::lined_buffer::LineNumber;
use ruff_python_ast::Stmt;
Expand Down Expand Up @@ -214,9 +218,16 @@ impl HoverValue {
let cleaned = doc.trim().replace('\n', " \n");
format!("{prefix}**Parameter `{}`**\n{}", name, cleaned)
});
let kind_formatted = self.kind.map_or("".to_owned(), |kind| {
format!("{} ", kind.display_for_hover())
});
let kind_formatted = self.kind.map_or_else(
|| {
if self.type_.is_function_type() {
"(function) ".to_owned()
} else {
String::new()
}
},
|kind| format!("{} ", kind.display_for_hover()),
);
let name_formatted = self
.name
.as_ref()
Expand All @@ -226,10 +237,10 @@ impl HoverValue {
} else {
String::new()
};
let type_display = self
.display
.clone()
.unwrap_or_else(|| self.type_.as_hover_string());
let type_display = self.display.clone().unwrap_or_else(|| {
self.type_
.as_hover_string_with_fallback_name(self.name.as_deref())
});

Hover {
contents: HoverContents::Markup(MarkupContent {
Expand Down Expand Up @@ -292,6 +303,99 @@ fn expand_callable_kwargs_for_hover<'a>(
}
}
}

/// If we can't determine a symbol name via go-to-definition, fall back to what the
/// type metadata knows about the callable. This primarily handles third-party stubs
/// where we only have typeshed information.
fn fallback_hover_name_from_type(type_: &Type) -> Option<String> {
match type_ {
Type::Function(function) => Some(
function
.metadata
.kind
.function_name()
.into_owned()
.to_string(),
),
Type::BoundMethod(bound_method) => match &bound_method.func {
BoundMethodType::Function(function) => Some(
function
.metadata
.kind
.function_name()
.into_owned()
.to_string(),
),
BoundMethodType::Forall(forall) => Some(
forall
.body
.metadata
.kind
.function_name()
.into_owned()
.to_string(),
),
BoundMethodType::Overload(overload) => Some(
overload
.metadata
.kind
.function_name()
.into_owned()
.to_string(),
),
},
Type::Overload(overload) => Some(
overload
.metadata
.kind
.function_name()
.into_owned()
.to_string(),
),
Type::Forall(forall) => match &forall.body {
Forallable::Function(function) => Some(
function
.metadata
.kind
.function_name()
.into_owned()
.to_string(),
),
Forallable::Callable(_) | Forallable::TypeAlias(_) => None,
},
Type::Type(inner) => fallback_hover_name_from_type(inner),
_ => None,
}
}

/// Extract the identifier under the cursor directly from the file contents so we can
/// label hover results even when go-to-definition fails.
fn identifier_text_at(
transaction: &Transaction<'_>,
handle: &Handle,
position: TextSize,
) -> Option<String> {
let module = transaction.get_module_info(handle)?;
let contents = module.contents();
let bytes = contents.as_bytes();
let len = bytes.len();
let pos = position.to_usize().min(len);
let is_ident_char = |b: u8| b == b'_' || b.is_ascii_alphanumeric();
let mut start = pos;
while start > 0 && is_ident_char(bytes[start - 1]) {
start -= 1;
}
let mut end = pos;
while end < len && is_ident_char(bytes[end]) {
end += 1;
}
if start == end {
return None;
}
let range = TextRange::new(TextSize::new(start as u32), TextSize::new(end as u32));
Some(module.code_at(range).to_owned())
}

pub fn get_hover(
transaction: &Transaction<'_>,
handle: &Handle,
Expand Down Expand Up @@ -334,20 +438,13 @@ pub fn get_hover(

// Otherwise, fall through to the existing type hover logic
let type_ = transaction.get_type_at(handle, position)?;
let type_display = transaction.ad_hoc_solve(handle, {
let mut cloned = type_.clone();
move |solver| {
// If the type is a callable, rewrite the signature to expand TypedDict-based
// `**kwargs` entries, ensuring hover text shows the actual keyword names users can pass.
cloned.visit_toplevel_callable_mut(|c| expand_callable_kwargs_for_hover(&solver, c));
cloned.as_hover_string()
}
});
let fallback_name_from_type = fallback_hover_name_from_type(&type_);
let (kind, name, docstring_range, module) = if let Some(FindDefinitionItemWithDocstring {
metadata,
definition_range: definition_location,
module,
docstring_range,
display_name,
}) = transaction
.find_definition(
handle,
Expand All @@ -365,16 +462,32 @@ pub fn get_hover(
if matches!(kind, Some(SymbolKind::Attribute)) && type_.is_function_type() {
kind = Some(SymbolKind::Method);
}
(
kind,
Some(module.code_at(definition_location).to_owned()),
docstring_range,
Some(module),
)
let name = {
let snippet = module.code_at(definition_location);
if snippet.chars().any(|c| !c.is_whitespace()) {
Some(snippet.to_owned())
} else if let Some(name) = display_name.clone() {
Some(name)
} else {
fallback_name_from_type.clone()
}
};
(kind, name, docstring_range, Some(module))
} else {
(None, None, None, None)
(None, fallback_name_from_type, None, None)
};

let name = name.or_else(|| identifier_text_at(transaction, handle, position));

let name_for_display = name.clone();
let type_display = transaction.ad_hoc_solve(handle, {
let mut cloned = type_.clone();
move |solver| {
cloned.visit_toplevel_callable_mut(|c| expand_callable_kwargs_for_hover(&solver, c));
cloned.as_hover_string_with_fallback_name(name_for_display.as_deref())
}
});

let docstring = if let (Some(docstring), Some(module)) = (docstring_range, module) {
Some(Docstring(docstring, module))
} else {
Expand Down Expand Up @@ -490,3 +603,55 @@ fn parameter_documentation_for_callee(
let docs = parse_parameter_documentation(module.code_at(range));
if docs.is_empty() { None } else { Some(docs) }
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::Arc;

use pyrefly_python::module::Module;
use pyrefly_python::module_path::ModulePath;
use pyrefly_types::callable::Callable;
use pyrefly_types::callable::FuncFlags;
use pyrefly_types::callable::FuncId;
use pyrefly_types::callable::FuncMetadata;
use pyrefly_types::callable::Function;
use pyrefly_types::callable::FunctionKind;
use ruff_python_ast::name::Name;

use super::*;

fn make_function_type(module_name: &str, func_name: &str) -> Type {
let module = Module::new(
ModuleName::from_str(module_name),
ModulePath::filesystem(PathBuf::from(format!("{module_name}.pyi"))),
Arc::new(String::new()),
);
let metadata = FuncMetadata {
kind: FunctionKind::Def(Box::new(FuncId {
module,
cls: None,
name: Name::new(func_name),
})),
flags: FuncFlags::default(),
};
Type::Function(Box::new(Function {
signature: Callable::ellipsis(Type::None),
metadata,
}))
}

#[test]
fn fallback_uses_function_metadata() {
let ty = make_function_type("numpy", "arange");
let fallback = fallback_hover_name_from_type(&ty);
assert_eq!(fallback.as_deref(), Some("arange"));
}

#[test]
fn fallback_recurses_through_type_wrapper() {
let ty = Type::Type(Box::new(make_function_type("pkg.subpkg", "run")));
let fallback = fallback_hover_name_from_type(&ty);
assert_eq!(fallback.as_deref(), Some("run"));
}
}
Loading