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
13 kB 426 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, visit::Visit}, 13 build::Module, 14 line_numbers::LineNumbers, 15 reference::ReferenceKind, 16 type_::{ModuleInterface, error::Named}, 17}; 18 19use crate::reference::{self, FindTypeVariableReferences, ModuleNameReferenceKind}; 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 module_alias: &EcoString, 350) -> RenameOutcome { 351 let new_name = EcoString::from(&params.new_name); 352 if name::check_name_case(SrcSpan::default(), &new_name, Named::Variable).is_err() { 353 return RenameOutcome::InvalidName { name: new_name }; 354 } 355 356 let uri = params 357 .text_document_position_params 358 .text_document 359 .uri 360 .clone(); 361 let mut edits = TextEdits::new(line_numbers); 362 363 let mut finder = reference::FindModuleNameReferences { 364 references: Vec::new(), 365 module_name, 366 module_alias, 367 }; 368 finder.visit_typed_module(&module.ast); 369 370 let original_module_name = module_name.split('/').next_back().unwrap_or(""); 371 372 for reference in finder.references { 373 match reference.kind { 374 ModuleNameReferenceKind::Import => { 375 edits.insert(reference.location.end, format!(" as {}", &params.new_name)) 376 } 377 ModuleNameReferenceKind::AliasedImport => { 378 if params.new_name == original_module_name { 379 edits.delete(SrcSpan::new( 380 reference.location.start - 1, 381 reference.location.end, 382 )); 383 } else { 384 edits.replace(reference.location, format!("as {}", &params.new_name)) 385 } 386 } 387 ModuleNameReferenceKind::ModuleSelect => { 388 edits.replace(reference.location, params.new_name.to_string()) 389 } 390 } 391 } 392 393 RenameOutcome::Renamed { 394 edit: workspace_edit(uri, edits.edits), 395 } 396} 397 398pub fn rename_type_variable( 399 module: &Module, 400 line_numbers: &LineNumbers, 401 params: &RenameParams, 402 location: SrcSpan, 403 name: EcoString, 404) -> RenameOutcome { 405 let new_name = EcoString::from(&params.new_name); 406 if name::check_name_case(Default::default(), &new_name, Named::TypeVariable).is_err() { 407 return RenameOutcome::InvalidName { name: new_name }; 408 } 409 410 let uri = params 411 .text_document_position_params 412 .text_document 413 .uri 414 .clone(); 415 let mut edits = TextEdits::new(line_numbers); 416 417 let references = FindTypeVariableReferences::find_in_module(&module.ast, location, &name); 418 419 for reference in references { 420 edits.replace(reference, params.new_name.clone()) 421 } 422 423 RenameOutcome::Renamed { 424 edit: workspace_edit(uri, edits.edits), 425 } 426}