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