Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / language-server / src / rename.rs
16 kB 506 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2025 The Gleam contributors 3 4use std::collections::HashMap; 5 6use ecow::EcoString; 7use lsp_server::ResponseError; 8use lsp_types::{Range, RenameParams, TextEdit, Uri as Url, WorkspaceEdit}; 9 10use gleam_core::{ 11 analyse::name, 12 ast::{self, SrcSpan}, 13 build::Module, 14 line_numbers::LineNumbers, 15 reference::{ModuleNameReference, ReferenceKind}, 16 type_::{ModuleInterface, error::Named}, 17}; 18 19use crate::reference::FindTypeVariableReferences; 20 21use super::{ 22 TextEdits, 23 compiler::ModuleSourceInformation, 24 edits::{self, Newlines, add_newlines_after_import, position_of_first_definition_if_import}, 25 reference::FindVariableReferences, 26 reference::VariableReferenceKind, 27 url_from_path, 28}; 29 30fn workspace_edit(uri: Url, edits: Vec<TextEdit>) -> WorkspaceEdit { 31 let mut changes = HashMap::new(); 32 let _ = changes.insert(uri, edits); 33 34 WorkspaceEdit { 35 changes: Some(changes), 36 document_changes: None, 37 change_annotations: None, 38 } 39} 40 41pub enum RenameOutcome { 42 InvalidName { name: EcoString }, 43 NoRenames, 44 Renamed { edit: WorkspaceEdit }, 45} 46 47/// Error code for when a request has invalid params as described in: 48/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#errorCodes 49/// 50const INVALID_PARAMS: i32 = -32602; 51 52impl RenameOutcome { 53 /// Turns the outcome of renaming into a value that's suitable to be used as 54 /// a response in the language server engine. 55 /// 56 pub fn into_result(self) -> Result<Option<WorkspaceEdit>, ResponseError> { 57 match self { 58 RenameOutcome::NoRenames => Ok(None), 59 RenameOutcome::Renamed { edit } => Ok(Some(edit)), 60 RenameOutcome::InvalidName { name } => Err(ResponseError { 61 code: INVALID_PARAMS, 62 message: format!("{name} is not a valid name"), 63 data: None, 64 }), 65 } 66 } 67} 68 69pub fn rename_local_variable( 70 module: &Module, 71 line_numbers: &LineNumbers, 72 params: &RenameParams, 73 definition_location: SrcSpan, 74 name: EcoString, 75 kind: VariableReferenceKind, 76) -> RenameOutcome { 77 let new_name = EcoString::from(&params.new_name); 78 if name::check_name_case(Default::default(), &new_name, Named::Variable).is_err() { 79 return RenameOutcome::InvalidName { name: new_name }; 80 } 81 82 let uri = params 83 .text_document_position_params 84 .text_document 85 .uri 86 .clone(); 87 let mut edits = TextEdits::new(line_numbers); 88 89 let references = 90 FindVariableReferences::new(definition_location, name).find_in_module(&module.ast); 91 92 match kind { 93 VariableReferenceKind::Variable => { 94 edits.replace(definition_location, params.new_name.clone()) 95 } 96 VariableReferenceKind::LabelShorthand => { 97 edits.insert(definition_location.end, format!(" {}", params.new_name)) 98 } 99 } 100 101 for reference in references { 102 match reference.kind { 103 VariableReferenceKind::Variable => { 104 edits.replace(reference.location, params.new_name.clone()) 105 } 106 VariableReferenceKind::LabelShorthand => { 107 edits.insert(reference.location.end, format!(" {}", params.new_name)) 108 } 109 } 110 } 111 112 RenameOutcome::Renamed { 113 edit: workspace_edit(uri, edits.edits), 114 } 115} 116 117#[derive(Debug)] 118pub enum RenameTarget { 119 Qualified, 120 Unqualified, 121 Definition, 122} 123 124pub struct Renamed<'a> { 125 pub module_name: &'a EcoString, 126 pub name: &'a EcoString, 127 pub name_kind: Named, 128 pub target_kind: RenameTarget, 129 pub layer: ast::Layer, 130} 131 132pub fn rename_module_entity( 133 params: &RenameParams, 134 current_module: &Module, 135 modules: &im::HashMap<EcoString, ModuleInterface>, 136 sources: &HashMap<EcoString, ModuleSourceInformation>, 137 renamed: Renamed<'_>, 138) -> RenameOutcome { 139 let new_name = EcoString::from(&params.new_name); 140 if name::check_name_case( 141 // We don't care about the actual error here, just whether the name is valid, 142 // so we just use the default span. 143 SrcSpan::default(), 144 &new_name, 145 renamed.name_kind, 146 ) 147 .is_err() 148 { 149 return RenameOutcome::InvalidName { name: new_name }; 150 } 151 152 match renamed.target_kind { 153 // When renaming an unqualified import, instead of renaming the original 154 // value, we simply want to alias it in the current module. 155 // It's an unqualified import if we are referencing it using unqualified 156 // syntax, and it is from a different module. 157 RenameTarget::Unqualified if renamed.module_name != &current_module.name => { 158 return alias_references_in_module( 159 params, 160 current_module, 161 renamed.module_name, 162 renamed.name, 163 renamed.layer, 164 ); 165 } 166 RenameTarget::Unqualified | RenameTarget::Qualified | RenameTarget::Definition => {} 167 } 168 169 let mut workspace_edit = WorkspaceEdit { 170 changes: Some(HashMap::new()), 171 document_changes: None, 172 change_annotations: None, 173 }; 174 175 for module in modules.values() { 176 if &module.name == renamed.module_name 177 || module 178 .references 179 .imported_modules 180 .contains(renamed.module_name) 181 { 182 let Some(source_information) = sources.get(&module.name) else { 183 continue; 184 }; 185 186 rename_references_in_module( 187 module, 188 source_information, 189 &mut workspace_edit, 190 renamed.module_name, 191 renamed.name, 192 params.new_name.clone(), 193 renamed.layer, 194 ); 195 } 196 } 197 198 RenameOutcome::Renamed { 199 edit: workspace_edit, 200 } 201} 202 203fn rename_references_in_module( 204 module: &ModuleInterface, 205 source_information: &ModuleSourceInformation, 206 workspace_edit: &mut WorkspaceEdit, 207 module_name: &EcoString, 208 name: &EcoString, 209 new_name: String, 210 layer: ast::Layer, 211) { 212 let reference_map = match layer { 213 ast::Layer::Value => &module.references.value_references, 214 ast::Layer::Type => &module.references.type_references, 215 }; 216 217 let Some(references) = reference_map.get(&(module_name.clone(), name.clone())) else { 218 return; 219 }; 220 221 let mut edits = TextEdits::new(&source_information.line_numbers); 222 223 for reference in references { 224 match reference.kind { 225 // If the reference is an alias, the alias name will remain unchanged. 226 ReferenceKind::Alias => {} 227 ReferenceKind::Qualified { .. } 228 | ReferenceKind::Unqualified 229 | ReferenceKind::Import 230 | ReferenceKind::Definition => edits.replace(reference.location, new_name.clone()), 231 } 232 } 233 234 let Some(uri) = url_from_path(source_information.path.as_str()) else { 235 return; 236 }; 237 238 if let Some(changes) = workspace_edit.changes.as_mut() { 239 _ = changes.insert(uri, edits.edits); 240 } 241} 242 243fn alias_references_in_module( 244 params: &RenameParams, 245 module: &Module, 246 module_name: &EcoString, 247 name: &EcoString, 248 layer: ast::Layer, 249) -> RenameOutcome { 250 let reference_map = match layer { 251 ast::Layer::Value => &module.ast.type_info.references.value_references, 252 ast::Layer::Type => &module.ast.type_info.references.type_references, 253 }; 254 255 let Some(references) = reference_map.get(&(module_name.clone(), name.clone())) else { 256 return RenameOutcome::NoRenames; 257 }; 258 259 let mut edits = TextEdits::new(&module.ast.type_info.line_numbers); 260 let mut found_import = false; 261 262 for reference in references { 263 match reference.kind { 264 ReferenceKind::Qualified { .. } => {} 265 ReferenceKind::Unqualified | ReferenceKind::Alias => { 266 edits.replace(reference.location, params.new_name.clone()) 267 } 268 ReferenceKind::Import => { 269 edits.insert(reference.location.end, format!(" as {}", params.new_name)); 270 found_import = true; 271 } 272 ReferenceKind::Definition => {} 273 } 274 } 275 276 // If we didn't find the import for the aliased type or value, then this is 277 // a prelude value and we need to add the import so we can alias it. 278 if !found_import { 279 let unqualified_import = match layer { 280 ast::Layer::Value => format!("{name} as {}", params.new_name), 281 ast::Layer::Type => format!("type {name} as {}", params.new_name), 282 }; 283 284 let import = module 285 .ast 286 .definitions 287 .imports 288 .iter() 289 .find(|import| import.module == *module_name); 290 291 if let Some(import) = import { 292 let (position, new_text) = 293 edits::insert_unqualified_import(import, &module.code, unqualified_import); 294 edits.insert(position, new_text); 295 } else { 296 add_import(module, module_name, unqualified_import, &mut edits); 297 } 298 } 299 300 RenameOutcome::Renamed { 301 edit: workspace_edit( 302 params 303 .text_document_position_params 304 .text_document 305 .uri 306 .clone(), 307 edits.edits, 308 ), 309 } 310} 311 312fn add_import( 313 module: &Module, 314 module_name: &EcoString, 315 unqualified_import: String, 316 edits: &mut TextEdits<'_>, 317) { 318 let position_of_first_import_if_present = 319 position_of_first_definition_if_import(module, &module.ast.type_info.line_numbers); 320 let first_is_import = position_of_first_import_if_present.is_some(); 321 let import_location = position_of_first_import_if_present.unwrap_or_default(); 322 323 let after_import_newlines = add_newlines_after_import( 324 import_location, 325 first_is_import, 326 &module.ast.type_info.line_numbers, 327 &module.code, 328 ); 329 330 let newlines = match after_import_newlines { 331 Newlines::Single => "\n", 332 Newlines::Double => "\n\n", 333 }; 334 335 edits.edits.push(TextEdit { 336 range: Range { 337 start: import_location, 338 end: import_location, 339 }, 340 new_text: format!("import {module_name}.{{{unqualified_import}}}{newlines}",), 341 }); 342} 343 344pub fn rename_module_alias( 345 module: &Module, 346 line_numbers: &LineNumbers, 347 params: &RenameParams, 348 module_name: &EcoString, 349) -> RenameOutcome { 350 let new_name = EcoString::from(&params.new_name); 351 if name::check_name_case(SrcSpan::default(), &new_name, Named::Variable).is_err() { 352 return RenameOutcome::InvalidName { name: new_name }; 353 } 354 355 let uri = params 356 .text_document_position_params 357 .text_document 358 .uri 359 .clone(); 360 let mut edits = TextEdits::new(line_numbers); 361 362 let original_module_name = module_name.split('/').next_back().unwrap_or(""); 363 364 let Some(references) = module 365 .ast 366 .type_info 367 .references 368 .module_references 369 .get(module_name) 370 else { 371 return RenameOutcome::Renamed { 372 edit: workspace_edit(uri, edits.edits), 373 }; 374 }; 375 376 for reference in references { 377 match reference { 378 ModuleNameReference::Import { 379 module_location: _, 380 import_end, 381 } => edits.insert(*import_end, format!(" as {}", &params.new_name)), 382 ModuleNameReference::AliasedImport { 383 alias_location, 384 module_location: _, 385 alias: _, 386 } => { 387 if params.new_name == original_module_name { 388 edits.delete(SrcSpan::new(alias_location.start - 1, alias_location.end)); 389 } else { 390 edits.replace(*alias_location, format!("as {}", &params.new_name)) 391 } 392 } 393 ModuleNameReference::ModuleSelect(location) 394 | ModuleNameReference::AliasedModuleSelect(location) => { 395 edits.replace(*location, params.new_name.to_string()); 396 } 397 } 398 } 399 400 RenameOutcome::Renamed { 401 edit: workspace_edit(uri, edits.edits), 402 } 403} 404 405pub fn rename_type_variable( 406 module: &Module, 407 line_numbers: &LineNumbers, 408 params: &RenameParams, 409 location: SrcSpan, 410 name: EcoString, 411) -> RenameOutcome { 412 let new_name = EcoString::from(&params.new_name); 413 if name::check_name_case(Default::default(), &new_name, Named::TypeVariable).is_err() { 414 return RenameOutcome::InvalidName { name: new_name }; 415 } 416 417 let uri = params 418 .text_document_position_params 419 .text_document 420 .uri 421 .clone(); 422 let mut edits = TextEdits::new(line_numbers); 423 424 let references = FindTypeVariableReferences::find_in_module(&module.ast, location, &name); 425 426 for reference in references { 427 edits.replace(reference, params.new_name.clone()) 428 } 429 430 RenameOutcome::Renamed { 431 edit: workspace_edit(uri, edits.edits), 432 } 433} 434 435pub fn rename_module_occurrences( 436 old_name: EcoString, 437 new_name: EcoString, 438 modules: &im::HashMap<EcoString, ModuleInterface>, 439 sources: &HashMap<EcoString, ModuleSourceInformation>, 440 changes: &mut HashMap<Url, Vec<TextEdit>>, 441) { 442 let name_parts = new_name.split('/'); 443 for part in name_parts { 444 if name::check_name_case(SrcSpan::default(), &part.into(), Named::Variable).is_err() { 445 return; 446 } 447 } 448 449 let last_component_of_new_name = new_name 450 .split('/') 451 .next_back() 452 .unwrap_or(&new_name) 453 .to_string(); 454 455 for module in modules.values() { 456 if !module.references.imported_modules.contains(&old_name) { 457 continue; 458 } 459 460 let Some(source_information) = sources.get(&module.name) else { 461 continue; 462 }; 463 464 let Some(references) = module.references.module_references.get(&old_name) else { 465 continue; 466 }; 467 468 let Some(uri) = url_from_path(source_information.path.as_str()) else { 469 continue; 470 }; 471 472 let mut edits = TextEdits::new(&source_information.line_numbers); 473 474 for reference in references { 475 match reference { 476 ModuleNameReference::Import { 477 module_location: location, 478 import_end: _, 479 } => edits.replace(*location, new_name.to_string()), 480 ModuleNameReference::AliasedImport { 481 module_location: location, 482 alias_location, 483 alias, 484 } => { 485 edits.replace(*location, new_name.to_string()); 486 // If we've imported a module using an alias, for example 487 // `import wibble as wobble`, and we then rename the file 488 // to `wobble.gleam`, the alias is no longer needed as the 489 // name is already `wobble`. 490 if *alias == last_component_of_new_name { 491 edits.delete(SrcSpan::new(alias_location.start - 1, alias_location.end)); 492 } 493 } 494 // If we've imported a module using an alias, we don't touch the 495 // alias, so any expressions referencing the alias name don't need 496 // to change. 497 ModuleNameReference::AliasedModuleSelect(_) => {} 498 ModuleNameReference::ModuleSelect(location) => { 499 edits.replace(*location, last_component_of_new_name.clone()) 500 } 501 } 502 } 503 504 changes.entry(uri).or_default().extend(edits.edits); 505 } 506}