| 
 | 1 | +/*  | 
 | 2 | + * Copyright (c) Meta Platforms, Inc. and affiliates.  | 
 | 3 | + *  | 
 | 4 | + * This source code is licensed under the MIT license found in the  | 
 | 5 | + * LICENSE file in the root directory of this source tree.  | 
 | 6 | + */  | 
 | 7 | + | 
 | 8 | +use std::collections::HashMap;  | 
 | 9 | +use std::sync::Arc;  | 
 | 10 | + | 
 | 11 | +use lsp_types::DocumentChangeOperation;  | 
 | 12 | +use lsp_types::DocumentChanges;  | 
 | 13 | +use lsp_types::OneOf;  | 
 | 14 | +use lsp_types::OptionalVersionedTextDocumentIdentifier;  | 
 | 15 | +use lsp_types::RenameFilesParams;  | 
 | 16 | +use lsp_types::TextDocumentEdit;  | 
 | 17 | +use lsp_types::TextEdit;  | 
 | 18 | +use lsp_types::Url;  | 
 | 19 | +use lsp_types::WorkspaceEdit;  | 
 | 20 | +use pyrefly_python::PYTHON_EXTENSIONS;  | 
 | 21 | +use pyrefly_python::ast::Ast;  | 
 | 22 | +use pyrefly_python::module_name::ModuleName;  | 
 | 23 | +use pyrefly_python::module_path::ModulePath;  | 
 | 24 | +use pyrefly_util::lined_buffer::LinedBuffer;  | 
 | 25 | +use pyrefly_util::lock::RwLock;  | 
 | 26 | +use rayon::prelude::*;  | 
 | 27 | +use ruff_python_ast::Stmt;  | 
 | 28 | +use ruff_text_size::Ranged;  | 
 | 29 | + | 
 | 30 | +use crate::lsp::module_helpers::handle_from_module_path;  | 
 | 31 | +use crate::lsp::module_helpers::module_info_to_uri;  | 
 | 32 | +use crate::state::state::State;  | 
 | 33 | +use crate::state::state::Transaction;  | 
 | 34 | + | 
 | 35 | +/// Visitor that looks for imports of an old module name and creates TextEdits to update them  | 
 | 36 | +struct RenameUsageVisitor<'a> {  | 
 | 37 | +    edits: Vec<TextEdit>,  | 
 | 38 | +    old_module_name: &'a ModuleName,  | 
 | 39 | +    new_module_name: &'a ModuleName,  | 
 | 40 | +    lined_buffer: &'a LinedBuffer,  | 
 | 41 | +}  | 
 | 42 | + | 
 | 43 | +impl<'a> RenameUsageVisitor<'a> {  | 
 | 44 | +    fn new(  | 
 | 45 | +        old_module_name: &'a ModuleName,  | 
 | 46 | +        new_module_name: &'a ModuleName,  | 
 | 47 | +        lined_buffer: &'a LinedBuffer,  | 
 | 48 | +    ) -> Self {  | 
 | 49 | +        Self {  | 
 | 50 | +            edits: Vec::new(),  | 
 | 51 | +            old_module_name,  | 
 | 52 | +            new_module_name,  | 
 | 53 | +            lined_buffer,  | 
 | 54 | +        }  | 
 | 55 | +    }  | 
 | 56 | + | 
 | 57 | +    fn visit_stmt(&mut self, stmt: &Stmt) {  | 
 | 58 | +        match stmt {  | 
 | 59 | +            Stmt::Import(import) => {  | 
 | 60 | +                for alias in &import.names {  | 
 | 61 | +                    let imported_module = ModuleName::from_name(&alias.name.id);  | 
 | 62 | +                    if imported_module == *self.old_module_name  | 
 | 63 | +                        || imported_module  | 
 | 64 | +                            .as_str()  | 
 | 65 | +                            .starts_with(&format!("{}.", self.old_module_name.as_str()))  | 
 | 66 | +                    {  | 
 | 67 | +                        // Replace the module name  | 
 | 68 | +                        let new_import_name = if imported_module == *self.old_module_name {  | 
 | 69 | +                            self.new_module_name.as_str().to_owned()  | 
 | 70 | +                        } else {  | 
 | 71 | +                            // Replace the prefix  | 
 | 72 | +                            imported_module.as_str().replace(  | 
 | 73 | +                                self.old_module_name.as_str(),  | 
 | 74 | +                                self.new_module_name.as_str(),  | 
 | 75 | +                            )  | 
 | 76 | +                        };  | 
 | 77 | + | 
 | 78 | +                        self.edits.push(TextEdit {  | 
 | 79 | +                            range: self.lined_buffer.to_lsp_range(alias.name.range()),  | 
 | 80 | +                            new_text: new_import_name,  | 
 | 81 | +                        });  | 
 | 82 | +                    }  | 
 | 83 | +                }  | 
 | 84 | +            }  | 
 | 85 | +            Stmt::ImportFrom(import_from) => {  | 
 | 86 | +                if let Some(module) = &import_from.module {  | 
 | 87 | +                    let imported_module = ModuleName::from_name(&module.id);  | 
 | 88 | +                    if imported_module == *self.old_module_name  | 
 | 89 | +                        || imported_module  | 
 | 90 | +                            .as_str()  | 
 | 91 | +                            .starts_with(&format!("{}.", self.old_module_name.as_str()))  | 
 | 92 | +                    {  | 
 | 93 | +                        // Replace the module name  | 
 | 94 | +                        let new_import_name = if imported_module == *self.old_module_name {  | 
 | 95 | +                            self.new_module_name.as_str().to_owned()  | 
 | 96 | +                        } else {  | 
 | 97 | +                            // Replace the prefix  | 
 | 98 | +                            imported_module.as_str().replace(  | 
 | 99 | +                                self.old_module_name.as_str(),  | 
 | 100 | +                                self.new_module_name.as_str(),  | 
 | 101 | +                            )  | 
 | 102 | +                        };  | 
 | 103 | + | 
 | 104 | +                        self.edits.push(TextEdit {  | 
 | 105 | +                            range: self.lined_buffer.to_lsp_range(module.range()),  | 
 | 106 | +                            new_text: new_import_name,  | 
 | 107 | +                        });  | 
 | 108 | +                    }  | 
 | 109 | +                }  | 
 | 110 | +            }  | 
 | 111 | +            _ => {}  | 
 | 112 | +        }  | 
 | 113 | +    }  | 
 | 114 | + | 
 | 115 | +    fn take_edits(self) -> Vec<TextEdit> {  | 
 | 116 | +        self.edits  | 
 | 117 | +    }  | 
 | 118 | +}  | 
 | 119 | + | 
 | 120 | +/// Handle workspace/willRenameFiles request to update imports when files are renamed.  | 
 | 121 | +///  | 
 | 122 | +/// This function:  | 
 | 123 | +/// 1. Converts file paths to module names  | 
 | 124 | +/// 2. Uses get_transitive_rdeps to find all files that depend on the renamed module  | 
 | 125 | +/// 3. Uses a visitor pattern to find imports of the old module and creates TextEdits  | 
 | 126 | +/// 4. Returns a WorkspaceEdit with all necessary changes  | 
 | 127 | +///  | 
 | 128 | +/// If the client supports `workspace.workspaceEdit.documentChanges`, the response will use  | 
 | 129 | +/// `document_changes` instead of `changes` for better ordering guarantees and version checking.  | 
 | 130 | +pub fn will_rename_files(  | 
 | 131 | +    state: &Arc<State>,  | 
 | 132 | +    transaction: &Transaction<'_>,  | 
 | 133 | +    _open_files: &Arc<RwLock<HashMap<std::path::PathBuf, Arc<String>>>>,  | 
 | 134 | +    params: RenameFilesParams,  | 
 | 135 | +    supports_document_changes: bool,  | 
 | 136 | +) -> Option<WorkspaceEdit> {  | 
 | 137 | +    eprintln!(  | 
 | 138 | +        "will_rename_files called with {} file(s)",  | 
 | 139 | +        params.files.len()  | 
 | 140 | +    );  | 
 | 141 | + | 
 | 142 | +    let mut all_changes: HashMap<Url, Vec<TextEdit>> = HashMap::new();  | 
 | 143 | + | 
 | 144 | +    for file_rename in ¶ms.files {  | 
 | 145 | +        eprintln!(  | 
 | 146 | +            "  Processing rename: {} -> {}",  | 
 | 147 | +            file_rename.old_uri, file_rename.new_uri  | 
 | 148 | +        );  | 
 | 149 | + | 
 | 150 | +        // Convert URLs to paths  | 
 | 151 | +        let old_uri = match Url::parse(&file_rename.old_uri) {  | 
 | 152 | +            Ok(uri) => uri,  | 
 | 153 | +            Err(_) => {  | 
 | 154 | +                eprintln!("    Failed to parse old_uri");  | 
 | 155 | +                continue;  | 
 | 156 | +            }  | 
 | 157 | +        };  | 
 | 158 | + | 
 | 159 | +        let new_uri = match Url::parse(&file_rename.new_uri) {  | 
 | 160 | +            Ok(uri) => uri,  | 
 | 161 | +            Err(_) => {  | 
 | 162 | +                eprintln!("    Failed to parse new_uri");  | 
 | 163 | +                continue;  | 
 | 164 | +            }  | 
 | 165 | +        };  | 
 | 166 | + | 
 | 167 | +        let old_path = match old_uri.to_file_path() {  | 
 | 168 | +            Ok(path) => path,  | 
 | 169 | +            Err(_) => {  | 
 | 170 | +                eprintln!("    Failed to convert old_uri to path");  | 
 | 171 | +                continue;  | 
 | 172 | +            }  | 
 | 173 | +        };  | 
 | 174 | + | 
 | 175 | +        let new_path = match new_uri.to_file_path() {  | 
 | 176 | +            Ok(path) => path,  | 
 | 177 | +            Err(_) => {  | 
 | 178 | +                eprintln!("    Failed to convert new_uri to path");  | 
 | 179 | +                continue;  | 
 | 180 | +            }  | 
 | 181 | +        };  | 
 | 182 | + | 
 | 183 | +        // Only process Python files  | 
 | 184 | +        if !PYTHON_EXTENSIONS  | 
 | 185 | +            .iter()  | 
 | 186 | +            .any(|ext| old_path.extension().and_then(|e| e.to_str()) == Some(*ext))  | 
 | 187 | +        {  | 
 | 188 | +            eprintln!("    Skipping non-Python file");  | 
 | 189 | +            continue;  | 
 | 190 | +        }  | 
 | 191 | + | 
 | 192 | +        // Important: only use filesystem handle (never use an in-memory handle)  | 
 | 193 | +        let old_handle = handle_from_module_path(state, ModulePath::filesystem(old_path.clone()));  | 
 | 194 | +        let config = state  | 
 | 195 | +            .config_finder()  | 
 | 196 | +            .python_file(old_handle.module(), old_handle.path());  | 
 | 197 | + | 
 | 198 | +        // Convert paths to module names  | 
 | 199 | +        let old_module_name =  | 
 | 200 | +            ModuleName::from_path(&old_path, config.search_path()).or_else(|| {  | 
 | 201 | +                // Fallback: try to get module name from the handle  | 
 | 202 | +                Some(old_handle.module())  | 
 | 203 | +            });  | 
 | 204 | + | 
 | 205 | +        // For the new module name, we can't rely on from_path because the file doesn't exist yet.  | 
 | 206 | +        // Instead, we compute the relative path from the old to new file and adjust the module name.  | 
 | 207 | +        let new_module_name = ModuleName::from_path(&new_path, config.search_path());  | 
 | 208 | + | 
 | 209 | +        let (old_module_name, new_module_name) = match (old_module_name, new_module_name) {  | 
 | 210 | +            (Some(old), Some(new)) => (old, new),  | 
 | 211 | +            _ => {  | 
 | 212 | +                eprintln!(  | 
 | 213 | +                    "    Could not determine module names for the rename (old={:?}, new={:?})",  | 
 | 214 | +                    old_module_name, new_module_name  | 
 | 215 | +                );  | 
 | 216 | +                continue;  | 
 | 217 | +            }  | 
 | 218 | +        };  | 
 | 219 | + | 
 | 220 | +        eprintln!(  | 
 | 221 | +            "    Module rename: {} -> {}",  | 
 | 222 | +            old_module_name, new_module_name  | 
 | 223 | +        );  | 
 | 224 | + | 
 | 225 | +        // If module names are the same, no need to update imports  | 
 | 226 | +        if old_module_name == new_module_name {  | 
 | 227 | +            eprintln!("    Module names are the same, skipping");  | 
 | 228 | +            continue;  | 
 | 229 | +        }  | 
 | 230 | + | 
 | 231 | +        // Use get_transitive_rdeps to find all files that depend on this module  | 
 | 232 | +        let rdeps = transaction.get_transitive_rdeps(old_handle.clone());  | 
 | 233 | + | 
 | 234 | +        eprintln!("    Found {} transitive rdeps", rdeps.len());  | 
 | 235 | + | 
 | 236 | +        // Visit each dependent file to find and update imports (parallelized)  | 
 | 237 | +        let rdeps_changes: Vec<(Url, Vec<TextEdit>)> = rdeps  | 
 | 238 | +            .into_par_iter()  | 
 | 239 | +            .filter_map(|rdep_handle| {  | 
 | 240 | +                let module_info = transaction.get_module_info(&rdep_handle)?;  | 
 | 241 | + | 
 | 242 | +                let ast = Ast::parse(module_info.contents()).0;  | 
 | 243 | +                let mut visitor = RenameUsageVisitor::new(  | 
 | 244 | +                    &old_module_name,  | 
 | 245 | +                    &new_module_name,  | 
 | 246 | +                    module_info.lined_buffer(),  | 
 | 247 | +                );  | 
 | 248 | + | 
 | 249 | +                for stmt in &ast.body {  | 
 | 250 | +                    visitor.visit_stmt(stmt);  | 
 | 251 | +                }  | 
 | 252 | + | 
 | 253 | +                let edits_for_file = visitor.take_edits();  | 
 | 254 | + | 
 | 255 | +                if !edits_for_file.is_empty() {  | 
 | 256 | +                    let uri = module_info_to_uri(&module_info)?;  | 
 | 257 | +                    eprintln!(  | 
 | 258 | +                        "    Found {} import(s) to update in {}",  | 
 | 259 | +                        edits_for_file.len(),  | 
 | 260 | +                        uri  | 
 | 261 | +                    );  | 
 | 262 | +                    Some((uri, edits_for_file))  | 
 | 263 | +                } else {  | 
 | 264 | +                    None  | 
 | 265 | +                }  | 
 | 266 | +            })  | 
 | 267 | +            .collect();  | 
 | 268 | + | 
 | 269 | +        // Merge results into all_changes  | 
 | 270 | +        for (uri, edits) in rdeps_changes {  | 
 | 271 | +            all_changes.entry(uri).or_default().extend(edits);  | 
 | 272 | +        }  | 
 | 273 | +    }  | 
 | 274 | + | 
 | 275 | +    if all_changes.is_empty() {  | 
 | 276 | +        eprintln!("  No import updates needed");  | 
 | 277 | +        None  | 
 | 278 | +    } else {  | 
 | 279 | +        eprintln!(  | 
 | 280 | +            "  Returning {} file(s) with import updates",  | 
 | 281 | +            all_changes.len()  | 
 | 282 | +        );  | 
 | 283 | + | 
 | 284 | +        if supports_document_changes {  | 
 | 285 | +            // Use document_changes for better ordering guarantees and version checking  | 
 | 286 | +            let document_changes: Vec<DocumentChangeOperation> = all_changes  | 
 | 287 | +                .into_iter()  | 
 | 288 | +                .map(|(uri, edits)| {  | 
 | 289 | +                    DocumentChangeOperation::Edit(TextDocumentEdit {  | 
 | 290 | +                        text_document: OptionalVersionedTextDocumentIdentifier {  | 
 | 291 | +                            uri,  | 
 | 292 | +                            version: None, // None means "any version"  | 
 | 293 | +                        },  | 
 | 294 | +                        edits: edits.into_iter().map(OneOf::Left).collect(),  | 
 | 295 | +                    })  | 
 | 296 | +                })  | 
 | 297 | +                .collect();  | 
 | 298 | + | 
 | 299 | +            Some(WorkspaceEdit {  | 
 | 300 | +                document_changes: Some(DocumentChanges::Operations(document_changes)),  | 
 | 301 | +                ..Default::default()  | 
 | 302 | +            })  | 
 | 303 | +        } else {  | 
 | 304 | +            // Fall back to changes for older clients  | 
 | 305 | +            Some(WorkspaceEdit {  | 
 | 306 | +                changes: Some(all_changes),  | 
 | 307 | +                ..Default::default()  | 
 | 308 | +            })  | 
 | 309 | +        }  | 
 | 310 | +    }  | 
 | 311 | +}  | 
0 commit comments