Fork of daniellemaywood.uk/gleam — Wasm codegen work
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(¶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 module_alias: &EcoString,
350) -> RenameOutcome {
351 let new_name = EcoString::from(¶ms.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 {}", ¶ms.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 {}", ¶ms.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(¶ms.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}