diff --git a/pyrefly/lib/state/lsp.rs b/pyrefly/lib/state/lsp.rs index 8e8f8115c8..decb994e71 100644 --- a/pyrefly/lib/state/lsp.rs +++ b/pyrefly/lib/state/lsp.rs @@ -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> { @@ -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) } @@ -2970,7 +2995,12 @@ impl<'a> Transaction<'a> { include_declaration: bool, ) -> Option> { 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( @@ -3124,12 +3154,46 @@ impl<'a> Transaction<'a> { definition_range: TextRange, expected_name: &Name, ) -> Option> { - 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> { 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()); + } - 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()); @@ -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); } diff --git a/pyrefly/lib/test/lsp/lsp_interaction/rename.rs b/pyrefly/lib/test/lsp/lsp_interaction/rename.rs index 8527c7d91f..f471df9cb4 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/rename.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/rename.rs @@ -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::(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(); diff --git a/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/defs.py b/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/defs.py new file mode 100644 index 0000000000..64e8598c66 --- /dev/null +++ b/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/defs.py @@ -0,0 +1,2 @@ +def greet(name, message): + return f"{message}, {name}" diff --git a/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/pyrefly.toml b/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/pyrefly.toml new file mode 100644 index 0000000000..54029c46ff --- /dev/null +++ b/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/pyrefly.toml @@ -0,0 +1 @@ +search_path = ["."] diff --git a/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/uses.py b/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/uses.py new file mode 100644 index 0000000000..edd9626246 --- /dev/null +++ b/pyrefly/lib/test/lsp/lsp_interaction/test_files/rename_kwargs_across_files/uses.py @@ -0,0 +1,4 @@ +from defs import greet + +def call(): + return greet(name="Alice", message="Hello")