Fork of daniellemaywood.uk/gleam — Wasm codegen work
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(¶ms.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(¶ms.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 != ¤t_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(¶ms.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 {}", ¶ms.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 {}", ¶ms.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(¶ms.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}