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