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