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
77 changes: 70 additions & 7 deletions pyrefly/lib/state/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2880,6 +2880,7 @@ impl<'a> Transaction<'a> {
fn local_references_from_external_definition(
&self,
handle: &Handle,
definition_metadata: &DefinitionMetadata,
definition_range: TextRange,
module: &Module,
) -> Option<Vec<TextRange>> {
Expand Down Expand Up @@ -2913,6 +2914,30 @@ impl<'a> Transaction<'a> {
}
}
}

let symbol_kind = match definition_metadata {
DefinitionMetadata::Variable(kind) | DefinitionMetadata::VariableOrAttribute(kind) => {
*kind
}
_ => None,
};

if matches!(
symbol_kind,
Some(SymbolKind::Parameter) | Some(SymbolKind::Variable)
) {
let definition_name = Name::new(module.code_at(definition_range));
if let Some(kwarg_references) = self
.keyword_argument_references_from_parameter_definition(
handle,
module,
definition_range,
&definition_name,
)
{
references.extend(kwarg_references);
}
}
Some(references)
}

Expand Down Expand Up @@ -2970,7 +2995,12 @@ impl<'a> Transaction<'a> {
include_declaration: bool,
) -> Option<Vec<TextRange>> {
let mut references = if handle.path() != module.path() {
self.local_references_from_external_definition(handle, definition_range, module)?
self.local_references_from_external_definition(
handle,
&definition_metadata,
definition_range,
module,
)?
} else {
let definition_name = Name::new(module.code_at(definition_range));
self.local_references_from_local_definition(
Expand Down Expand Up @@ -3124,12 +3154,46 @@ impl<'a> Transaction<'a> {
definition_range: TextRange,
expected_name: &Name,
) -> Option<Vec<TextRange>> {
let ast = self.get_ast(handle)?;
let definition_module = self.get_module_info(handle)?;
self.keyword_argument_references_from_parameter_definition(
handle,
&definition_module,
definition_range,
expected_name,
)
}

fn keyword_argument_references_from_parameter_definition(
&self,
handle: &Handle,
definition_module: &ModuleInfo,
definition_range: TextRange,
expected_name: &Name,
) -> Option<Vec<TextRange>> {
let keyword_args = self.collect_local_keyword_arguments_by_name(handle, expected_name);
let mut references = Vec::new();
if keyword_args.is_empty() {
return Some(Vec::new());
}
Comment on lines 3173 to +3176
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keyword_argument_references_from_parameter_definition ultimately depends on collect_local_keyword_arguments_by_name(handle, ...), which returns an empty list when get_ast(handle) is unavailable. In this codebase, ASTs can be evicted after later pipeline steps (see ComputeGuard::evict_ast in state/module.rs), so for unopened modules (or modules whose AST was evicted) cross-file kwarg references will still be missed and rename won’t update those files. Consider adding the same fallback used for definition_ast: if get_ast(handle) is None, re-parse the referencing module from get_module_info(handle)?.contents() and use that AST to collect keyword args, so rename works even when the file wasn’t opened.

Copilot uses AI. Check for mistakes.

let definition_module = self.get_module_info(handle)?;
let definition_ast = if handle.path() == definition_module.path() {
self.get_ast(handle)?
} else {
let definition_handle = Handle::new(
definition_module.name(),
definition_module.path().dupe(),
handle.sys_info().dupe(),
);
self.get_ast(&definition_handle).unwrap_or_else(|| {
Ast::parse(
definition_module.contents(),
definition_module.source_type(),
)
.0
.into()
})
};

let mut references = Vec::new();
for (kw_identifier, callee_kind) in keyword_args {
let callee_locations =
self.get_callee_location(handle, &callee_kind, FindPreference::default());
Expand All @@ -3140,13 +3204,12 @@ impl<'a> Transaction<'a> {
} in callee_locations
{
if module.path() == definition_module.path() {
// Refine to get the actual parameter location
// Refine to get the actual parameter location.
if let Some(param_range) = self.refine_param_location_for_callee(
ast.as_ref(),
definition_ast.as_ref(),
callee_def_range,
&kw_identifier,
) {
// If the parameter location matches our definition, this is a valid reference
if param_range == definition_range {
references.push(kw_identifier.range);
}
Expand Down
59 changes: 59 additions & 0 deletions pyrefly/lib/test/lsp/lsp_interaction/rename.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,65 @@ fn test_rename_editable_package_symbols_is_allowed() {
interaction.shutdown().unwrap();
}

#[test]
fn test_rename_kwarg_across_files() {
let root = get_test_files_root();
let root_path = root.path().join("rename_kwargs_across_files");
let scope_uri = Url::from_file_path(root_path.clone()).unwrap();

let mut interaction = LspInteraction::new();
interaction.set_root(root_path.clone());
interaction
.initialize(InitializeSettings {
workspace_folders: Some(vec![("test".to_owned(), scope_uri.clone())]),
configuration: Some(Some(json!([{ "indexing_mode": "lazy_blocking" }]))),
..Default::default()
})
.unwrap();

let defs = root_path.join("defs.py");
let uses = root_path.join("uses.py");

interaction.client.did_open("defs.py");
interaction.client.did_open("uses.py");

interaction
.client
.send_request::<Rename>(json!({
"textDocument": {
"uri": Url::from_file_path(&defs).unwrap().to_string()
},
"position": {
"line": 0,
"character": 16
},
"newName": "note"
}))
.expect_response(json!({
"changes": {
Url::from_file_path(&defs).unwrap().to_string(): [
{
"newText":"note",
"range":{"start":{"line":0,"character":16},"end":{"line":0,"character":23}}
},
{
"newText":"note",
"range":{"start":{"line":1,"character":14},"end":{"line":1,"character":21}}
},
],
Url::from_file_path(&uses).unwrap().to_string(): [
{
"newText":"note",
"range":{"start":{"line":3,"character":31},"end":{"line":3,"character":38}}
},
]
}
}))
.unwrap();

interaction.shutdown().unwrap();
}

#[test]
fn test_rename() {
let root = get_test_files_root();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def greet(name, message):
return f"{message}, {name}"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
search_path = ["."]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from defs import greet

def call():
return greet(name="Alice", message="Hello")
Loading