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
20 kB 614 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::{LabelSyntax, ModuleNameReference, RecordLabel, 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(SrcSpan::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 243/// Renames a record field label across the whole project. 244/// 245/// Unlike module entities, labels are never imported or qualified, so every 246/// reference is simply replaced with the new name. The one special case is the 247/// label shorthand syntax (`wibble:`), which is expanded so the implicit value 248/// variable keeps its original name. 249/// 250pub fn rename_label( 251 params: &RenameParams, 252 type_module: &EcoString, 253 type_name: &EcoString, 254 label: &EcoString, 255 modules: &im::HashMap<EcoString, ModuleInterface>, 256 sources: &HashMap<EcoString, ModuleSourceInformation>, 257) -> RenameOutcome { 258 let new_name = EcoString::from(&params.new_name); 259 if name::check_name_case(SrcSpan::default(), &new_name, Named::Label).is_err() { 260 return RenameOutcome::InvalidName { name: new_name }; 261 } 262 263 let mut workspace_edit = WorkspaceEdit { 264 changes: Some(HashMap::new()), 265 document_changes: None, 266 change_annotations: None, 267 }; 268 269 let key = RecordLabel { 270 type_module: type_module.clone(), 271 type_name: type_name.clone(), 272 label: label.clone(), 273 }; 274 275 // A label can be referenced in a module that doesn't import the type's 276 // defining module: a record value can be obtained transitively through 277 // another module and have its fields accessed. So every module has to be 278 // searched. 279 for module in modules.values() { 280 let Some(source_information) = sources.get(&module.name) else { 281 continue; 282 }; 283 284 rename_label_references_in_module( 285 module, 286 source_information, 287 &mut workspace_edit, 288 &key, 289 label, 290 &params.new_name, 291 ); 292 } 293 294 RenameOutcome::Renamed { 295 edit: workspace_edit, 296 } 297} 298 299fn rename_label_references_in_module( 300 module: &ModuleInterface, 301 source_information: &ModuleSourceInformation, 302 workspace_edit: &mut WorkspaceEdit, 303 key: &RecordLabel, 304 label: &EcoString, 305 new_name: &str, 306) { 307 let definitions = module.references.label_definitions.get(key); 308 let references = module.references.label_references.get(key); 309 if definitions.is_none() && references.is_none() { 310 return; 311 } 312 313 let mut edits = TextEdits::new(&source_information.line_numbers); 314 315 // The definitions of the field are renamed along with its references. A 316 // field shared between multiple variants has a definition in each, and 317 // they are all renamed together so that code accessing the shared field 318 // keeps compiling. 319 for definition in definitions.into_iter().flatten() { 320 edits.replace(definition.location, new_name.to_string()); 321 } 322 323 for reference in references.into_iter().flatten() { 324 match reference.syntax { 325 // A label written using shorthand syntax (`wibble:`) relies on the 326 // field and the variable sharing a name. Renaming the field alone 327 // would break that, so we expand the shorthand and keep the original 328 // name as the value: `wibble:` becomes `new_name: wibble`. 329 LabelSyntax::Shorthand => { 330 edits.replace(reference.location, format!("{new_name}: {label}")) 331 } 332 LabelSyntax::Longhand => edits.replace(reference.location, new_name.to_string()), 333 } 334 } 335 336 let Some(uri) = url_from_path(source_information.path.as_str()) else { 337 return; 338 }; 339 340 if let Some(changes) = workspace_edit.changes.as_mut() { 341 _ = changes.insert(uri, edits.edits); 342 } 343} 344 345fn alias_references_in_module( 346 params: &RenameParams, 347 module: &Module, 348 module_name: &EcoString, 349 name: &EcoString, 350 layer: ast::Layer, 351) -> RenameOutcome { 352 let reference_map = match layer { 353 ast::Layer::Value => &module.ast.type_info.references.value_references, 354 ast::Layer::Type => &module.ast.type_info.references.type_references, 355 }; 356 357 let Some(references) = reference_map.get(&(module_name.clone(), name.clone())) else { 358 return RenameOutcome::NoRenames; 359 }; 360 361 let mut edits = TextEdits::new(&module.ast.type_info.line_numbers); 362 let mut found_import = false; 363 364 for reference in references { 365 match reference.kind { 366 ReferenceKind::Qualified { .. } => {} 367 ReferenceKind::Unqualified | ReferenceKind::Alias => { 368 edits.replace(reference.location, params.new_name.clone()) 369 } 370 ReferenceKind::Import(alias_location) => { 371 // If old name is equal to original name, we can just remove 372 // alias part. 373 if name == &params.new_name { 374 edits.delete(alias_location); 375 } else { 376 edits.replace(alias_location, format!(" as {}", params.new_name)); 377 } 378 found_import = true; 379 } 380 ReferenceKind::Definition => {} 381 } 382 } 383 384 // If we didn't find the import for the aliased type or value, then this is 385 // a prelude value and we need to add the import so we can alias it. 386 if !found_import { 387 let unqualified_import = match layer { 388 ast::Layer::Value => format!("{name} as {}", params.new_name), 389 ast::Layer::Type => format!("type {name} as {}", params.new_name), 390 }; 391 392 let import = module 393 .ast 394 .definitions 395 .imports 396 .iter() 397 .find(|import| import.module == *module_name); 398 399 if let Some(import) = import { 400 let (position, new_text) = 401 edits::insert_unqualified_import(import, &module.code, unqualified_import); 402 edits.insert(position, new_text); 403 } else { 404 add_import(module, module_name, unqualified_import, &mut edits); 405 } 406 } 407 408 RenameOutcome::Renamed { 409 edit: workspace_edit( 410 params 411 .text_document_position_params 412 .text_document 413 .uri 414 .clone(), 415 edits.edits, 416 ), 417 } 418} 419 420fn add_import( 421 module: &Module, 422 module_name: &EcoString, 423 unqualified_import: String, 424 edits: &mut TextEdits<'_>, 425) { 426 let position_of_first_import_if_present = 427 position_of_first_definition_if_import(module, &module.ast.type_info.line_numbers); 428 let first_is_import = position_of_first_import_if_present.is_some(); 429 let import_location = position_of_first_import_if_present.unwrap_or_default(); 430 431 let after_import_newlines = add_newlines_after_import( 432 import_location, 433 first_is_import, 434 &module.ast.type_info.line_numbers, 435 &module.code, 436 ); 437 438 let newlines = match after_import_newlines { 439 Newlines::Single => "\n", 440 Newlines::Double => "\n\n", 441 }; 442 443 edits.edits.push(TextEdit { 444 range: Range { 445 start: import_location, 446 end: import_location, 447 }, 448 new_text: format!("import {module_name}.{{{unqualified_import}}}{newlines}",), 449 }); 450} 451 452pub fn rename_module_alias( 453 module: &Module, 454 line_numbers: &LineNumbers, 455 params: &RenameParams, 456 module_name: &EcoString, 457) -> RenameOutcome { 458 let new_name = EcoString::from(&params.new_name); 459 if name::check_name_case(SrcSpan::default(), &new_name, Named::Variable).is_err() { 460 return RenameOutcome::InvalidName { name: new_name }; 461 } 462 463 let uri = params 464 .text_document_position_params 465 .text_document 466 .uri 467 .clone(); 468 let mut edits = TextEdits::new(line_numbers); 469 470 let original_module_name = module_name.split('/').next_back().unwrap_or(""); 471 472 let Some(references) = module 473 .ast 474 .type_info 475 .references 476 .module_references 477 .get(module_name) 478 else { 479 return RenameOutcome::Renamed { 480 edit: workspace_edit(uri, edits.edits), 481 }; 482 }; 483 484 for reference in references { 485 match reference { 486 ModuleNameReference::Import { 487 module_location: _, 488 import_end, 489 } => edits.insert(*import_end, format!(" as {}", &params.new_name)), 490 ModuleNameReference::AliasedImport { 491 alias_location, 492 module_location: _, 493 alias: _, 494 } => { 495 if params.new_name == original_module_name { 496 edits.delete(SrcSpan::new(alias_location.start - 1, alias_location.end)); 497 } else { 498 edits.replace(*alias_location, format!("as {}", &params.new_name)) 499 } 500 } 501 ModuleNameReference::ModuleSelect(location) 502 | ModuleNameReference::AliasedModuleSelect(location) => { 503 edits.replace(*location, params.new_name.to_string()); 504 } 505 } 506 } 507 508 RenameOutcome::Renamed { 509 edit: workspace_edit(uri, edits.edits), 510 } 511} 512 513pub fn rename_type_variable( 514 module: &Module, 515 line_numbers: &LineNumbers, 516 params: &RenameParams, 517 location: SrcSpan, 518 name: EcoString, 519) -> RenameOutcome { 520 let new_name = EcoString::from(&params.new_name); 521 if name::check_name_case(SrcSpan::default(), &new_name, Named::TypeVariable).is_err() { 522 return RenameOutcome::InvalidName { name: new_name }; 523 } 524 525 let uri = params 526 .text_document_position_params 527 .text_document 528 .uri 529 .clone(); 530 let mut edits = TextEdits::new(line_numbers); 531 532 let references = FindTypeVariableReferences::find_in_module(&module.ast, location, &name); 533 534 for reference in references { 535 edits.replace(reference, params.new_name.clone()) 536 } 537 538 RenameOutcome::Renamed { 539 edit: workspace_edit(uri, edits.edits), 540 } 541} 542 543pub fn rename_module_occurrences( 544 old_name: EcoString, 545 new_name: EcoString, 546 modules: &im::HashMap<EcoString, ModuleInterface>, 547 sources: &HashMap<EcoString, ModuleSourceInformation>, 548 changes: &mut HashMap<Url, Vec<TextEdit>>, 549) { 550 let name_parts = new_name.split('/'); 551 for part in name_parts { 552 if name::check_name_case(SrcSpan::default(), &part.into(), Named::Variable).is_err() { 553 return; 554 } 555 } 556 557 let last_component_of_new_name = new_name 558 .split('/') 559 .next_back() 560 .unwrap_or(&new_name) 561 .to_string(); 562 563 for module in modules.values() { 564 if !module.references.imported_modules.contains(&old_name) { 565 continue; 566 } 567 568 let Some(source_information) = sources.get(&module.name) else { 569 continue; 570 }; 571 572 let Some(references) = module.references.module_references.get(&old_name) else { 573 continue; 574 }; 575 576 let Some(uri) = url_from_path(source_information.path.as_str()) else { 577 continue; 578 }; 579 580 let mut edits = TextEdits::new(&source_information.line_numbers); 581 582 for reference in references { 583 match reference { 584 ModuleNameReference::Import { 585 module_location: location, 586 import_end: _, 587 } => edits.replace(*location, new_name.to_string()), 588 ModuleNameReference::AliasedImport { 589 module_location: location, 590 alias_location, 591 alias, 592 } => { 593 edits.replace(*location, new_name.to_string()); 594 // If we've imported a module using an alias, for example 595 // `import wibble as wobble`, and we then rename the file 596 // to `wobble.gleam`, the alias is no longer needed as the 597 // name is already `wobble`. 598 if *alias == last_component_of_new_name { 599 edits.delete(SrcSpan::new(alias_location.start - 1, alias_location.end)); 600 } 601 } 602 // If we've imported a module using an alias, we don't touch the 603 // alias, so any expressions referencing the alias name don't need 604 // to change. 605 ModuleNameReference::AliasedModuleSelect(_) => {} 606 ModuleNameReference::ModuleSelect(location) => { 607 edits.replace(*location, last_component_of_new_name.clone()) 608 } 609 } 610 } 611 612 changes.entry(uri).or_default().extend(edits.edits); 613 } 614}