diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c0aa6e5c..f6fbcbb2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,29 @@ ([Surya Rose](https://github.com/GearsDatapacks)) +- The language server now automatically updates imports when a Gleam module is + renamed. For example: + + ```gleam + import db_users + + pub fn main() -> db_users.User { + db_users.new("username") + } + ``` + + Renaming `db_users.gleam` to `database/user.gleam` would produce: + + ```gleam + import database/user + + pub fn main() -> user.User { + user.new("username") + } + ``` + + ([Surya Rose](https://github.com/GearsDatapacks)) + ### Formatter ### Bug fixes diff --git a/compiler-core/src/analyse.rs b/compiler-core/src/analyse.rs index b54cbdbbf..f7f4defcf 100644 --- a/compiler-core/src/analyse.rs +++ b/compiler-core/src/analyse.rs @@ -409,6 +409,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> { .collect(), value_references: env.references.value_references, type_references: env.references.type_references, + module_references: env.references.module_references, }, inline_functions: self.inline_functions, }, diff --git a/compiler-core/src/analyse/imports.rs b/compiler-core/src/analyse/imports.rs index 664136630..cdfb38de0 100644 --- a/compiler-core/src/analyse/imports.rs +++ b/compiler-core/src/analyse/imports.rs @@ -298,12 +298,15 @@ impl<'context, 'problems> Importer<'context, 'problems> { import.module.clone(), alias_location, import.location, + import.module_location, ); } else { self.environment.references.register_module( used_name.clone(), import.module.clone(), import.location, + import.module_location, + None, ); } diff --git a/compiler-core/src/metadata/tests.rs b/compiler-core/src/metadata/tests.rs index fadbf6c73..d51117696 100644 --- a/compiler-core/src/metadata/tests.rs +++ b/compiler-core/src/metadata/tests.rs @@ -15,7 +15,7 @@ use crate::{ build::Origin, line_numbers::LineNumbers, parse::LiteralFloatValue, - reference::{Reference, ReferenceKind}, + reference::{ModuleNameReference, Reference, ReferenceKind}, type_::{ self, Deprecation, ModuleInterface, Opaque, References, Type, TypeAliasConstructor, TypeConstructor, TypeValueConstructor, TypeValueConstructorField, TypeVariantConstructors, @@ -1985,15 +1985,24 @@ fn module_with_references() { vec![ Reference { location: SrcSpan::new(26, 35), - kind: ReferenceKind::Qualified, + kind: ReferenceKind::Qualified { + module_alias: "some_other_module".into(), + module_location: SrcSpan::new(26, 29), + }, }, Reference { location: SrcSpan::new(152, 204), - kind: ReferenceKind::Qualified, + kind: ReferenceKind::Qualified { + module_alias: "some_other_module".into(), + module_location: SrcSpan::new(26, 29), + }, }, Reference { location: SrcSpan::new(0, 8), - kind: ReferenceKind::Qualified, + kind: ReferenceKind::Qualified { + module_alias: "some_alias".into(), + module_location: SrcSpan::new(26, 29), + }, }, ], ), @@ -2004,7 +2013,10 @@ fn module_with_references() { vec![ Reference { location: SrcSpan::new(26, 35), - kind: ReferenceKind::Qualified, + kind: ReferenceKind::Qualified { + module_alias: "some_alias".into(), + module_location: SrcSpan::new(26, 29), + }, }, Reference { location: SrcSpan::new(152, 204), @@ -2017,6 +2029,23 @@ fn module_with_references() { ], )] .into(), + module_references: [( + "some_module".into(), + vec![ + ModuleNameReference::Import { + module_location: SrcSpan::new(5, 20), + import_end: 31, + }, + ModuleNameReference::AliasedImport { + module_location: SrcSpan::new(5, 20), + alias_location: SrcSpan::new(26, 32), + alias: "some_alias".into(), + }, + ModuleNameReference::ModuleSelect(SrcSpan::new(92, 100)), + ModuleNameReference::AliasedModuleSelect(SrcSpan::new(152, 160)), + ], + )] + .into(), }, inline_functions: HashMap::new(), }; diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs index fa8e7f863..881872c53 100644 --- a/compiler-core/src/parse.rs +++ b/compiler-core/src/parse.rs @@ -3006,7 +3006,6 @@ where let mut start = 0; let mut end; let mut module = EcoString::new(); - let mut last_segment_start; let mut last_segment_end; // Gather module names @@ -3019,7 +3018,6 @@ where } module.push_str(&name); end = e; - last_segment_start = s; last_segment_end = e; // Useful error for : import a/.{b} @@ -3100,7 +3098,7 @@ where end, }, module_location: SrcSpan { - start: last_segment_start, + start, end: last_segment_end, }, unqualified_values, diff --git a/compiler-core/src/reference.rs b/compiler-core/src/reference.rs index 5c11bcd2f..e5202869c 100644 --- a/compiler-core/src/reference.rs +++ b/compiler-core/src/reference.rs @@ -11,21 +11,170 @@ use petgraph::{ stable_graph::{NodeIndex, StableGraph}, }; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +/// Describes one of a number of situations where references can be generated. +/// See each variant for an explanation. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum ReferenceKind { - Qualified, + /// A type or value which is referenced using the qualified syntax, along with + /// some information about the qualifier which is used for tracking module name + /// references. For example: + /// ```gleam + /// import gleam/option.{Some} + /// + /// pub fn main() -> option.Option(Int) { + /// // ^^^^^^ `module_location` covers this + /// Some(1) + /// } + /// ``` + /// + /// Here, `module_alias` is `option`, as that's what's being used as a + /// qualifier. + /// + /// + /// ```gleam + /// import gleam/int as integer + /// + /// pub fn main() { + /// integer.add(1, 2) + /// //^^^^^^^ `module_location` covers this + /// } + /// ``` + /// + /// In this case, `module_alias` is `integer`, due to the aliased import. + /// + Qualified { + module_alias: EcoString, + module_location: SrcSpan, + }, + /// A type or value is being referenced using unqualified syntax. This may + /// be due to being imported unqualified, or because it's from the same + /// module. For example: + /// + /// ```gleam + /// import gleam/option.{None} + /// + /// pub fn main() { + /// none() + /// //^^^^ Unqualified + /// } + /// + /// fn none() { + /// None + /// //^^^^ Unqualified + /// } + /// ``` + /// Unqualified, + /// A value or type is being referenced inside an unqualified import. For + /// example: + /// ```gleam + /// import gleam/option.{None} + /// // ^^^^ Import + /// import gleam/dynamic/decode.{type Dynamic} + /// // ^^^^^^^ Import + /// ``` Import, + /// The original definition location of a type or value. This also counts as + /// a reference for renaming and "find references" purposes. For example: + /// + /// ```gleam + /// pub type Wibble { + /// // ^^^^^^ Definition + /// Wibble(Int) + /// //^^^^^^ Definition + /// } + /// + /// pub fn extract(w: Wibble) { + /// // ^^^^^^^ Definition + /// let Wibble(x) = w + /// x + /// } + /// ``` Definition, + /// A value or type is being referenced using unqualified syntax, with a + /// name other than its original definition. This can be due to importing it + /// using an alias, or due to referencing it through a type alias. For example: + /// + /// ```gleam + /// import gleam/option.{None as Nothing, type Option as Maybe} + /// + /// pub fn nothing() -> Maybe(_) { + /// // ^^^^^ Alias + /// Nothing + /// //^^^^^^^ Alias + /// } + /// ``` + /// Alias, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct Reference { pub location: SrcSpan, pub kind: ReferenceKind, } +/// A reference to a module name. This is similar to a `Reference`, which covers +/// types and values, but it is separate because we care about slightly different +/// pieces of information when, for example, renaming modules vs. renaming types +/// or values. +/// +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum ModuleNameReference { + /// The location of a module name in a `ModuleSelect`, when the module name + /// is not aliased. For example: + /// ```gleam + /// import gleam/option + /// + /// pub fn main() -> option.Option(_) { + /// // ^^^^^^ ModuleSelect + /// option.None + /// //^^^^^^ ModuleSelect + /// } + /// ``` + /// + ModuleSelect(SrcSpan), + /// The location of a module name in a `ModuleSelect`, when the module name + /// *is* aliased. For example: + /// ```gleam + /// import gleam/option as maybe + /// + /// pub fn main() -> maybe.Option(_) { + /// // ^^^^^ AliasedModuleSelect + /// maybe.None + /// //^^^^^ AliasedModuleSelect + /// } + /// ``` + /// + AliasedModuleSelect(SrcSpan), + /// The location of a module name in an `import` statement, when the module + /// name is not aliased. For example: + /// ```gleam + /// import gleam/option.{None, Some} + /// // ^^^^^^^^^^^^ Import ^ `import_end` + /// ``` + /// + Import { + module_location: SrcSpan, + import_end: u32, + }, + /// The location of a module name in an `import` statement, when the module + /// name *is* aliased. Also stores the location of the alias (including the + /// `as` keyword), and what the alias is. For example: + /// ```gleam + /// import gleam/option.{None, Some} as maybe + /// // ^^^^^^^^^^^^ `module_location` + /// // ^^^^^^^^ `alias_location` + /// ``` + /// In this example, `alias` would be `maybe`. + /// + AliasedImport { + module_location: SrcSpan, + alias_location: SrcSpan, + alias: EcoString, + }, +} + pub type ReferenceMap = HashMap<(EcoString, EcoString), Vec>; #[derive(Debug, Clone)] @@ -96,6 +245,9 @@ pub struct ReferenceTracker { /// The locations of the references to each type in this module, used for /// renaming and go-to reference. pub type_references: ReferenceMap, + /// The locations of the references to each imported module, used for + /// renaming and go-to reference. + pub module_references: HashMap>, /// This map is used to access the nodes of modules that were not /// aliased, given their name. @@ -280,11 +432,16 @@ impl ReferenceTracker { module_name: EcoString, alias_location: SrcSpan, import_location: SrcSpan, + module_location: SrcSpan, ) { - // We first record a node for the module being aliased. We use its entire - // name to identify it in this case and keep track of the node it's - // associated with. - self.register_module(module_name.clone(), module_name.clone(), import_location); + // We first record a node for the module being aliased. + self.register_module( + used_name.clone(), + module_name.clone(), + import_location, + module_location, + Some(alias_location), + ); // Then we create a node for the alias, as the alias itself might be // unused! @@ -315,12 +472,29 @@ impl ReferenceTracker { used_name: EcoString, module_name: EcoString, location: SrcSpan, + module_location: SrcSpan, + alias_location: Option, ) { self.current_node = self.create_node(used_name.clone(), EntityLayer::Module); let _ = self .module_name_to_node .insert(module_name.clone(), self.current_node); + let reference = if let Some(alias_location) = alias_location { + ModuleNameReference::AliasedImport { + module_location, + alias_location, + alias: used_name.clone(), + } + } else { + ModuleNameReference::Import { + module_location, + import_end: location.end, + } + }; + + self.register_module_name_reference(module_name.clone(), reference); + let entity = Entity { name: used_name, layer: EntityLayer::Module, @@ -360,8 +534,20 @@ impl ReferenceTracker { location: SrcSpan, kind: ReferenceKind, ) { - match kind { - ReferenceKind::Qualified | ReferenceKind::Import | ReferenceKind::Definition => {} + match &kind { + ReferenceKind::Qualified { + module_alias, + module_location, + } => { + let last_module_segment = module.split('/').next_back().unwrap_or(&module); + let reference = if last_module_segment == module_alias { + ModuleNameReference::ModuleSelect(*module_location) + } else { + ModuleNameReference::AliasedModuleSelect(*module_location) + }; + self.register_module_name_reference(module.clone(), reference); + } + ReferenceKind::Import | ReferenceKind::Definition => {} ReferenceKind::Alias | ReferenceKind::Unqualified => { let target = self.get_or_create_node(referenced_name.clone(), EntityLayer::Value); _ = self.graph.add_edge(self.current_node, target, ()); @@ -382,8 +568,20 @@ impl ReferenceTracker { location: SrcSpan, kind: ReferenceKind, ) { - match kind { - ReferenceKind::Qualified | ReferenceKind::Import | ReferenceKind::Definition => {} + match &kind { + ReferenceKind::Qualified { + module_alias, + module_location, + } => { + let last_module_segment = module.split('/').next_back().unwrap_or(&module); + let reference = if last_module_segment == module_alias { + ModuleNameReference::ModuleSelect(*module_location) + } else { + ModuleNameReference::AliasedModuleSelect(*module_location) + }; + self.register_module_name_reference(module.clone(), reference); + } + ReferenceKind::Import | ReferenceKind::Definition => {} ReferenceKind::Alias | ReferenceKind::Unqualified => { self.register_type_reference_in_call_graph(referenced_name.clone()) } @@ -395,6 +593,22 @@ impl ReferenceTracker { .push(Reference { location, kind }); } + /// Register a reference to a module in the code. This is separate to + /// `register_module_reference`, as references to modules can be created + /// implicitly, for example when using unqualified imports. This only register + /// explicit references in the source code, when the module name or local + /// alias is written. + pub fn register_module_name_reference( + &mut self, + module: EcoString, + reference: ModuleNameReference, + ) { + self.module_references + .entry(module) + .or_default() + .push(reference); + } + /// Like `register_type_reference`, but doesn't modify `self.type_references`. /// This is used when we define a constructor for a custom type. The constructor /// doesn't actually "reference" its type, but if the constructor is used, the diff --git a/compiler-core/src/type_.rs b/compiler-core/src/type_.rs index 7a6866833..c91c302be 100644 --- a/compiler-core/src/type_.rs +++ b/compiler-core/src/type_.rs @@ -35,7 +35,7 @@ use crate::{ build::{Origin, Target}, inline::InlinableFunction, line_numbers::LineNumbers, - reference::ReferenceMap, + reference::{ModuleNameReference, ReferenceMap}, type_::expression::Implementations, }; use error::*; @@ -1056,6 +1056,7 @@ pub struct References { pub imported_modules: HashSet, pub value_references: ReferenceMap, pub type_references: ReferenceMap, + pub module_references: HashMap>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs index 3209c5767..071fcb02f 100644 --- a/compiler-core/src/type_/expression.rs +++ b/compiler-core/src/type_/expression.rs @@ -1455,12 +1455,17 @@ impl<'a, 'b> ExprTyper<'a, 'b> { // We only register the reference here, if we know that this is a module access. // Otherwise we would register module access even if we are actually accessing // the field on a record + let module_location = + SrcSpan::new(location.start, location.start + module_alias.len() as u32); self.environment.references.register_value_reference( module_name.clone(), label.clone(), &label, SrcSpan::new(field_start, location.end), - ReferenceKind::Qualified, + ReferenceKind::Qualified { + module_alias: module_alias.clone(), + module_location, + }, ); TypedExpr::ModuleSelect { location, @@ -2577,7 +2582,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { match self.infer_clause_guard_variable(name.clone(), location) { // If the variable itself cannot be inferred as one, then // it could really be a module select. We try that one - // as an elternative. + // as an alternative. Err(error) => self.infer_guard_module_access( name, label, @@ -2594,7 +2599,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { } } else { // If it doesn't this has to be a regular record access and - // we try and inferr it as such. + // we try and infer it as such. let inferred_container = self.infer_clause_guard(*container.clone()); self.infer_guard_record_access( inferred_container, @@ -2854,7 +2859,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { label.clone(), &label, label_location, - ReferenceKind::Qualified, + ReferenceKind::Qualified { + module_alias: module_alias.clone(), + module_location, + }, ); Ok(ClauseGuard::ModuleSelect { @@ -3717,16 +3725,15 @@ impl<'a, 'b> ExprTyper<'a, 'b> { ReferenceRegistration::DoNotRegister => (), ReferenceRegistration::Register | ReferenceRegistration::VariableArgument { .. } => { - self.register_value_constructor_reference( - name, - &variant, - *location, - if module.is_some() { - ReferenceKind::Qualified - } else { - ReferenceKind::Unqualified - }, - ); + let kind = if let Some((module_alias, module_location)) = module { + ReferenceKind::Qualified { + module_alias: module_alias.clone(), + module_location: *module_location, + } + } else { + ReferenceKind::Unqualified + }; + self.register_value_constructor_reference(name, &variant, *location, kind); } } diff --git a/compiler-core/src/type_/hydrator.rs b/compiler-core/src/type_/hydrator.rs index 441227850..e6c8194db 100644 --- a/compiler-core/src/type_/hydrator.rs +++ b/compiler-core/src/type_/hydrator.rs @@ -179,8 +179,11 @@ impl Hydrator { .clone(); if let Some((type_module, type_name)) = return_type.named_type_name() { - let reference_kind = if module.is_some() { - ReferenceKind::Qualified + let reference_kind = if let Some((module_alias, module_location)) = &module { + ReferenceKind::Qualified { + module_alias: module_alias.clone(), + module_location: *module_location, + } } else if name != &type_name { ReferenceKind::Alias } else { diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs index ff553969a..d8e21870f 100644 --- a/compiler-core/src/type_/pattern.rs +++ b/compiler-core/src/type_/pattern.rs @@ -1188,16 +1188,20 @@ impl<'a, 'b> PatternTyper<'a, 'b> { } } + let kind = if let Some((module_alias, module_location)) = &module { + ReferenceKind::Qualified { + module_alias: module_alias.clone(), + module_location: *module_location, + } + } else { + ReferenceKind::Unqualified + }; self.environment.references.register_value_reference( pattern_constructor.module.clone(), pattern_constructor.name.clone(), &name, name_location, - if module.is_some() { - ReferenceKind::Qualified - } else { - ReferenceKind::Unqualified - }, + kind, ); let instantiated_constructor_type = diff --git a/language-server/src/engine.rs b/language-server/src/engine.rs index c1ceef0f8..0ca1380b6 100644 --- a/language-server/src/engine.rs +++ b/language-server/src/engine.rs @@ -33,12 +33,15 @@ use lsp_types::{ MarkupContent, Position, PrepareRenameResult, Range, SignatureHelp, SymbolKind, SymbolTag, TextEdit, Uri as Url, WorkspaceEdit, }; -use std::{collections::HashSet, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use crate::{ code_action::{RemoveRedundantRecordUpdate, ReplaceUnderscoreWithType, type_errors_for_module}, reference::find_module_references_in_module, - rename::{rename_module_alias, rename_type_variable}, + rename::{rename_module_alias, rename_module_occurrences, rename_type_variable}, }; use super::{ @@ -838,7 +841,17 @@ where None } } - Some(Referenced::ModuleName { location, .. }) => success_response(location), + Some(Referenced::ModuleName { + location, + module_alias, + .. + }) => success_response(SrcSpan::new( + // Since the location contains the full module name (e.g. `wibble/wobble/woo`), + // we just want to include the last segment so we get a rename of the string + // `woo`, as that's the being referenced in module access expressions. + location.end - module_alias.len() as u32, + location.end, + )), Some(Referenced::TypeVariable { location, name: _ }) => success_response(location), @@ -935,12 +948,9 @@ where ) .into_result(), - Some(Referenced::ModuleName { - module_name, - module_alias, - .. - }) => rename_module_alias(module, &lines, ¶ms, &module_name, &module_alias) - .into_result(), + Some(Referenced::ModuleName { module_name, .. }) => { + rename_module_alias(module, &lines, ¶ms, &module_name).into_result() + } Some(Referenced::TypeVariable { location, name }) => { rename_type_variable(module, &lines, ¶ms, location, name).into_result() @@ -1085,6 +1095,36 @@ where }) } + /// Triggers after the renaming of one or more `.gleam` files, updating any + /// imports to those modules. + pub fn rename_files(&mut self, renames: Vec<(Url, Url)>) -> Response> { + self.respond(|this| { + let mut changes = HashMap::new(); + + for (old_uri, new_uri) in renames { + let Some(old_name) = this.module_name_for_uri(&old_uri) else { + continue; + }; + let Some(new_name) = this.module_name_for_uri(&new_uri) else { + continue; + }; + rename_module_occurrences( + old_name, + new_name, + this.compiler.project_compiler.get_importable_modules(), + &this.compiler.sources, + &mut changes, + ); + } + + Ok(Some(WorkspaceEdit { + changes: Some(changes), + document_changes: None, + change_annotations: None, + })) + }) + } + fn respond(&mut self, handler: impl FnOnce(&mut Self) -> Result) -> Response { let result = handler(self); let warnings = self.take_warnings(); @@ -1308,7 +1348,7 @@ Unused labelled fields: self.module_node_at_position(params, module) } - fn module_for_uri(&self, uri: &Url) -> Option<&Module> { + fn module_name_for_uri(&self, uri: &Url) -> Option { // The to_file_path method is available on these platforms #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))] let path = uri.to_file_path().expect("URL file"); @@ -1322,13 +1362,23 @@ Unused labelled fields: .components() .skip(1) .map(|c| c.as_os_str().to_string_lossy()); - let module_name: EcoString = Itertools::intersperse(components, "/".into()) + let name = Itertools::intersperse(components, "/".into()) .collect::() .strip_suffix(".gleam")? .into(); + Some(name) + } + fn module_for_uri(&self, uri: &Url) -> Option<&Module> { + let module_name = self.module_name_for_uri(uri)?; self.compiler.modules.get(&module_name) } + + #[cfg(test)] + pub fn path_for_module_name(&self, module_name: &str) -> Utf8PathBuf { + let src_directory = self.paths.src_directory(); + src_directory.join(module_name).with_extension("gleam") + } } fn import_folding_spans( diff --git a/language-server/src/messages.rs b/language-server/src/messages.rs index d06fd3f80..1f3b0d871 100644 --- a/language-server/src/messages.rs +++ b/language-server/src/messages.rs @@ -8,7 +8,7 @@ use lsp_types::{ DidCloseTextDocumentNotification, DidSaveTextDocumentNotification, DocumentFormattingRequest, DocumentHighlightRequest, DocumentSymbolRequest, FoldingRangeRequest, HoverRequest, PrepareRenameRequest, ReferencesRequest, RenameRequest, SignatureHelpRequest, - TextDocumentContentChangeEvent, TypeDefinitionRequest, + TextDocumentContentChangeEvent, TypeDefinitionRequest, WillRenameFilesRequest, }; use std::time::Duration; @@ -33,6 +33,7 @@ pub enum Request { Rename(lsp::RenameParams), FindReferences(lsp::ReferenceParams), DocumentHighlight(lsp::DocumentHighlightParams), + RenameFiles(lsp::RenameFilesParams), } impl Request { @@ -91,6 +92,10 @@ impl Request { let params = cast_request::(request); Some(Message::Request(id, Request::DocumentHighlight(params))) } + "workspace/willRenameFiles" => { + let params = cast_request::(request); + Some(Message::Request(id, Request::RenameFiles(params))) + } _ => None, } } diff --git a/language-server/src/reference.rs b/language-server/src/reference.rs index 0481cec21..87db4d1fb 100644 --- a/language-server/src/reference.rs +++ b/language-server/src/reference.rs @@ -10,8 +10,7 @@ use gleam_core::{ analyse, ast::{ self, ArgNames, AssignName, BitArraySize, ClauseGuard, CustomType, Function, - ModuleConstant, Pattern, RecordConstructor, SrcSpan, TypeAstConstructorName, TypedExpr, - TypedModule, visit::Visit, + ModuleConstant, Pattern, RecordConstructor, SrcSpan, TypedExpr, TypedModule, visit::Visit, }, build::Located, type_::{ @@ -834,260 +833,6 @@ impl<'ast> Visit<'ast> for FindVariableReferences { } } -pub struct ModuleNameReference { - pub location: SrcSpan, - pub kind: ModuleNameReferenceKind, -} - -pub enum ModuleNameReferenceKind { - Import, - AliasedImport, - ModuleSelect, -} - -pub struct FindModuleNameReferences<'a> { - pub references: Vec, - pub module_name: &'a EcoString, - pub module_alias: &'a EcoString, -} - -impl<'ast> Visit<'ast> for FindModuleNameReferences<'_> { - fn visit_typed_module(&mut self, module: &'ast TypedModule) { - ast::visit::visit_typed_module(self, module); - } - - fn visit_typed_clause_guard(&mut self, guard: &'ast ast::TypedClauseGuard) { - ast::visit::visit_typed_clause_guard(self, guard); - } - - fn visit_typed_import(&mut self, import: &'ast ast::TypedImport) { - match import.as_name.as_ref() { - None => { - if import.module == *self.module_name { - self.references.push(ModuleNameReference { - location: import.location, - kind: ModuleNameReferenceKind::Import, - }) - } - } - Some((AssignName::Variable(alias) | AssignName::Discard(alias), alias_location)) => { - if alias == self.module_alias { - self.references.push(ModuleNameReference { - location: *alias_location, - kind: ModuleNameReferenceKind::AliasedImport, - }) - } - } - } - - ast::visit::visit_typed_import(self, import); - } - - fn visit_typed_clause_guard_module_select( - &mut self, - location: &'ast SrcSpan, - field_start: &'ast u32, - definition_location: &'ast SrcSpan, - type_: &'ast std::sync::Arc, - label: &'ast EcoString, - module_name: &'ast EcoString, - module_alias: &'ast EcoString, - literal: &'ast ast::TypedConstant, - ) { - if module_alias == self.module_alias { - self.references.push(ModuleNameReference { - location: SrcSpan::new( - location.start, - location.start + (module_alias.len() as u32), - ), - kind: ModuleNameReferenceKind::ModuleSelect, - }); - } - - ast::visit::visit_typed_clause_guard_module_select( - self, - location, - field_start, - definition_location, - type_, - label, - module_name, - module_alias, - literal, - ); - } - - fn visit_typed_expr_module_select( - &mut self, - location: &'ast SrcSpan, - field_start: &'ast u32, - type_: &'ast std::sync::Arc, - label: &'ast EcoString, - module_name: &'ast EcoString, - module_alias: &'ast EcoString, - constructor: &'ast ModuleValueConstructor, - ) { - if module_alias == self.module_alias { - self.references.push(ModuleNameReference { - location: SrcSpan::new( - location.start, - location.start + (module_alias.len() as u32), - ), - kind: ModuleNameReferenceKind::ModuleSelect, - }); - } - - ast::visit::visit_typed_expr_module_select( - self, - location, - field_start, - type_, - label, - module_name, - module_alias, - constructor, - ); - } - - fn visit_type_ast_constructor( - &mut self, - location: &'ast SrcSpan, - name: &'ast TypeAstConstructorName, - arguments: &'ast [ast::TypeAst], - arguments_types: Option>>, - ) { - if let TypeAstConstructorName::Qualified { - module: module_alias, - module_location, - .. - } = name - && module_alias == self.module_alias - { - self.references.push(ModuleNameReference { - location: *module_location, - kind: ModuleNameReferenceKind::ModuleSelect, - }) - } - - ast::visit::visit_type_ast_constructor(self, location, name, arguments, arguments_types); - } - - fn visit_typed_constant_record( - &mut self, - location: &'ast SrcSpan, - module: &'ast Option<(EcoString, SrcSpan)>, - name: &'ast EcoString, - arguments: &'ast Option>>, - type_: &'ast std::sync::Arc, - field_map: &'ast analyse::Inferred, - record_constructor: &'ast Option>, - ) { - if let Some((module_alias, module_location)) = module - && module_alias == self.module_alias - { - self.references.push(ModuleNameReference { - location: *module_location, - kind: ModuleNameReferenceKind::ModuleSelect, - }) - } - - ast::visit::visit_typed_constant_record( - self, - location, - module, - name, - arguments, - type_, - field_map, - record_constructor, - ); - } - - fn visit_typed_constant_record_update( - &mut self, - location: &'ast SrcSpan, - constructor_location: &'ast SrcSpan, - module: &'ast Option<(EcoString, SrcSpan)>, - name: &'ast EcoString, - record: &'ast ast::RecordBeingUpdated, - arguments: &'ast [ast::RecordUpdateArg], - type_: &'ast std::sync::Arc, - field_map: &'ast analyse::Inferred, - ) { - if let Some((module_alias, module_location)) = module - && module_alias == self.module_alias - { - self.references.push(ModuleNameReference { - location: *module_location, - kind: ModuleNameReferenceKind::ModuleSelect, - }) - } - - ast::visit::visit_typed_constant_record_update( - self, - location, - constructor_location, - module, - name, - record, - arguments, - type_, - field_map, - ) - } - - fn visit_typed_constant_var( - &mut self, - _location: &'ast SrcSpan, - module: &'ast Option<(EcoString, SrcSpan)>, - _name: &'ast EcoString, - _constructor: &'ast Option>, - _type_: &'ast std::sync::Arc, - ) { - if let Some((module_alias, module_location)) = module - && module_alias == self.module_alias - { - self.references.push(ModuleNameReference { - location: *module_location, - kind: ModuleNameReferenceKind::ModuleSelect, - }) - } - } - - fn visit_typed_pattern_constructor( - &mut self, - location: &'ast SrcSpan, - name_location: &'ast SrcSpan, - name: &'ast EcoString, - arguments: &'ast Vec>, - module: &'ast Option<(EcoString, SrcSpan)>, - constructor: &'ast analyse::Inferred, - spread: &'ast Option, - type_: &'ast std::sync::Arc, - ) { - if let Some((module_alias, module_location)) = module - && module_alias == self.module_alias - { - self.references.push(ModuleNameReference { - location: *module_location, - kind: ModuleNameReferenceKind::ModuleSelect, - }); - } - - ast::visit::visit_typed_pattern_constructor( - self, - location, - name_location, - name, - arguments, - module, - constructor, - spread, - type_, - ); - } -} - pub struct FindTypeVariableReferences<'a> { pub references: Vec, pub name: &'a EcoString, diff --git a/language-server/src/rename.rs b/language-server/src/rename.rs index 2f5bd9c0a..2222ed55c 100644 --- a/language-server/src/rename.rs +++ b/language-server/src/rename.rs @@ -9,14 +9,14 @@ use lsp_types::{Range, RenameParams, TextEdit, Uri as Url, WorkspaceEdit}; use gleam_core::{ analyse::name, - ast::{self, SrcSpan, visit::Visit}, + ast::{self, SrcSpan}, build::Module, line_numbers::LineNumbers, - reference::ReferenceKind, + reference::{ModuleNameReference, ReferenceKind}, type_::{ModuleInterface, error::Named}, }; -use crate::reference::{self, FindTypeVariableReferences, ModuleNameReferenceKind}; +use crate::reference::FindTypeVariableReferences; use super::{ TextEdits, @@ -224,7 +224,7 @@ fn rename_references_in_module( match reference.kind { // If the reference is an alias, the alias name will remain unchanged. ReferenceKind::Alias => {} - ReferenceKind::Qualified + ReferenceKind::Qualified { .. } | ReferenceKind::Unqualified | ReferenceKind::Import | ReferenceKind::Definition => edits.replace(reference.location, new_name.clone()), @@ -261,7 +261,7 @@ fn alias_references_in_module( for reference in references { match reference.kind { - ReferenceKind::Qualified => {} + ReferenceKind::Qualified { .. } => {} ReferenceKind::Unqualified | ReferenceKind::Alias => { edits.replace(reference.location, params.new_name.clone()) } @@ -346,7 +346,6 @@ pub fn rename_module_alias( line_numbers: &LineNumbers, params: &RenameParams, module_name: &EcoString, - module_alias: &EcoString, ) -> RenameOutcome { let new_name = EcoString::from(¶ms.new_name); if name::check_name_case(SrcSpan::default(), &new_name, Named::Variable).is_err() { @@ -360,32 +359,40 @@ pub fn rename_module_alias( .clone(); let mut edits = TextEdits::new(line_numbers); - let mut finder = reference::FindModuleNameReferences { - references: Vec::new(), - module_name, - module_alias, - }; - finder.visit_typed_module(&module.ast); - let original_module_name = module_name.split('/').next_back().unwrap_or(""); - for reference in finder.references { - match reference.kind { - ModuleNameReferenceKind::Import => { - edits.insert(reference.location.end, format!(" as {}", ¶ms.new_name)) - } - ModuleNameReferenceKind::AliasedImport => { + let Some(references) = module + .ast + .type_info + .references + .module_references + .get(module_name) + else { + return RenameOutcome::Renamed { + edit: workspace_edit(uri, edits.edits), + }; + }; + + for reference in references { + match reference { + ModuleNameReference::Import { + module_location: _, + import_end, + } => edits.insert(*import_end, format!(" as {}", ¶ms.new_name)), + ModuleNameReference::AliasedImport { + alias_location, + module_location: _, + alias: _, + } => { if params.new_name == original_module_name { - edits.delete(SrcSpan::new( - reference.location.start - 1, - reference.location.end, - )); + edits.delete(SrcSpan::new(alias_location.start - 1, alias_location.end)); } else { - edits.replace(reference.location, format!("as {}", ¶ms.new_name)) + edits.replace(*alias_location, format!("as {}", ¶ms.new_name)) } } - ModuleNameReferenceKind::ModuleSelect => { - edits.replace(reference.location, params.new_name.to_string()) + ModuleNameReference::ModuleSelect(location) + | ModuleNameReference::AliasedModuleSelect(location) => { + edits.replace(*location, params.new_name.to_string()); } } } @@ -424,3 +431,76 @@ pub fn rename_type_variable( edit: workspace_edit(uri, edits.edits), } } + +pub fn rename_module_occurrences( + old_name: EcoString, + new_name: EcoString, + modules: &im::HashMap, + sources: &HashMap, + changes: &mut HashMap>, +) { + let name_parts = new_name.split('/'); + for part in name_parts { + if name::check_name_case(SrcSpan::default(), &part.into(), Named::Variable).is_err() { + return; + } + } + + let last_component_of_new_name = new_name + .split('/') + .next_back() + .unwrap_or(&new_name) + .to_string(); + + for module in modules.values() { + if !module.references.imported_modules.contains(&old_name) { + continue; + } + + let Some(source_information) = sources.get(&module.name) else { + continue; + }; + + let Some(references) = module.references.module_references.get(&old_name) else { + continue; + }; + + let Some(uri) = url_from_path(source_information.path.as_str()) else { + continue; + }; + + let mut edits = TextEdits::new(&source_information.line_numbers); + + for reference in references { + match reference { + ModuleNameReference::Import { + module_location: location, + import_end: _, + } => edits.replace(*location, new_name.to_string()), + ModuleNameReference::AliasedImport { + module_location: location, + alias_location, + alias, + } => { + edits.replace(*location, new_name.to_string()); + // If we've imported a module using an alias, for example + // `import wibble as wobble`, and we then rename the file + // to `wobble.gleam`, the alias is no longer needed as the + // name is already `wobble`. + if *alias == last_component_of_new_name { + edits.delete(SrcSpan::new(alias_location.start - 1, alias_location.end)); + } + } + // If we've imported a module using an alias, we don't touch the + // alias, so any expressions referencing the alias name don't need + // to change. + ModuleNameReference::AliasedModuleSelect(_) => {} + ModuleNameReference::ModuleSelect(location) => { + edits.replace(*location, last_component_of_new_name.clone()) + } + } + } + + changes.entry(uri).or_default().extend(edits.edits); + } +} diff --git a/language-server/src/server.rs b/language-server/src/server.rs index 853c23de8..5de59bf3a 100644 --- a/language-server/src/server.rs +++ b/language-server/src/server.rs @@ -19,10 +19,13 @@ use gleam_core::{ io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter}, line_numbers::LineNumbers, }; +use itertools::Itertools; use lsp_server::ResponseError; use lsp_types::{ - self as lsp, InitializeParams, Position, PublishDiagnosticsParams, Range, RenameOptions, - TextEdit, Uri as Url, + self as lsp, FileOperationFilter, FileOperationOptions, FileOperationPattern, + FileOperationPatternKind, FileOperationRegistrationOptions, InitializeParams, Position, + PublishDiagnosticsParams, Range, RenameFilesParams, RenameOptions, TextEdit, Uri as Url, + WorkspaceOptions, }; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -113,6 +116,7 @@ where Request::GoToTypeDefinition(param) => self.goto_type_definition(param), Request::FindReferences(param) => self.find_references(param), Request::DocumentHighlight(param) => self.document_highlight(param), + Request::RenameFiles(param) => self.rename_files(param), }; self.publish_feedback(feedback); @@ -435,6 +439,30 @@ where ) } + fn rename_files( + &mut self, + params: RenameFilesParams, + ) -> (Result, Feedback) { + let renames = params + .files + .into_iter() + .map(|file| { + ( + Url::parse(&file.old_uri).expect("Uri should be valid"), + Url::parse(&file.new_uri).expect("Uri should be valid"), + ) + }) + .collect_vec(); + + let Some((_, first_renamed_file)) = renames.first() else { + return (Ok(serde_json::json!(null)), Feedback::none()); + }; + + self.respond_with_engine(super::path(first_renamed_file), |engine| { + engine.rename_files(renames) + }) + } + fn find_references( &mut self, params: lsp_types::ReferenceParams, @@ -567,7 +595,27 @@ fn initialisation_handshake(connection: &lsp_server::Connection) -> InitializePa folding_range_provider: Some(true.into()), declaration_provider: None, execute_command_provider: None, - workspace: None, + workspace: Some(WorkspaceOptions { + workspace_folders: None, + file_operations: Some(FileOperationOptions { + did_create: None, + will_create: None, + will_rename: Some(FileOperationRegistrationOptions { + filters: vec![FileOperationFilter { + scheme: Some("file".into()), + pattern: FileOperationPattern { + glob: "**/*.gleam".into(), + matches: Some(FileOperationPatternKind::File), + options: None, + }, + }], + }), + did_rename: None, + did_delete: None, + will_delete: None, + }), + text_document_content: None, + }), call_hierarchy_provider: None, semantic_tokens_provider: None, moniker_provider: None, diff --git a/language-server/src/tests.rs b/language-server/src/tests.rs index a5ef5f8d7..61b9a6804 100644 --- a/language-server/src/tests.rs +++ b/language-server/src/tests.rs @@ -809,6 +809,20 @@ impl<'a> TestProject<'a> { executor(&mut engine, params, code.into()) } + + /// Run a test in a project without a specific position (for workspace-wide + /// actions). + pub fn run( + &self, + executor: impl FnOnce( + &mut LanguageServerEngine, + ) -> T, + ) -> T { + // Use a throwaway position and ignore it + let (mut engine, _) = self.positioned_with_io(Position::default()); + + executor(&mut engine) + } } #[derive(Clone)] diff --git a/language-server/src/tests/rename.rs b/language-server/src/tests/rename.rs index 745841d9b..2a898109a 100644 --- a/language-server/src/tests/rename.rs +++ b/language-server/src/tests/rename.rs @@ -5,9 +5,11 @@ use std::collections::HashMap; use lsp_types::{ Position, PrepareRenameParams, PrepareRenamePlaceholder, Range, RenameParams, - TextDocumentPositionParams, Uri as Url, WorkDoneProgressParams, + TextDocumentPositionParams, Uri as Url, WorkDoneProgressParams, WorkspaceEdit, }; +use crate::url_from_path; + use super::{TestProject, find_position_of, hover}; /// Returns the rename range and edit to apply if the rename is valid and can be @@ -18,7 +20,7 @@ fn rename( tester: &TestProject<'_>, new_name: &str, position: Position, -) -> Result, String> { +) -> Result, String> { let prepare_rename_response = tester.at(position, |engine, params, _| { let params = PrepareRenameParams { text_document_position_params: TextDocumentPositionParams { @@ -59,6 +61,29 @@ fn rename( } } +fn rename_files(tester: &TestProject<'_>, renames: &[(&str, &str)]) -> HashMap { + let edit = tester.run(|engine| { + let params = renames + .iter() + .map(|(old_name, new_name)| { + let old_url = + url_from_path(engine.path_for_module_name(old_name).as_str()).unwrap(); + let new_url = + url_from_path(engine.path_for_module_name(new_name).as_str()).unwrap(); + (old_url, new_url) + }) + .collect(); + engine.rename_files(params).result.unwrap() + }); + + let changes = edit + .expect("No text edit found") + .changes + .expect("No text edit found"); + + apply_code_edit(tester, changes) +} + fn apply_rename( tester: &TestProject<'_>, new_name: &str, @@ -86,6 +111,51 @@ fn apply_code_edit( modules } +fn display_result( + project: &TestProject<'_>, + modules: HashMap, + renamed_modules: HashMap<&str, &str>, + range: Option, +) -> String { + let mut output = String::from("----- BEFORE RENAME\n"); + for (name, src) in project.root_package_modules.iter() { + output.push_str(&format!("-- {name}.gleam\n{src}\n\n")); + } + + let src = project.src; + let app_src_before = if let Some(range) = range { + hover::show_hover(src, range, range.start) + } else { + src.to_string() + }; + output.push_str(&format!( + "-- app.gleam\n{app_src_before}\n\n----- AFTER RENAME\n", + )); + + for &(name, src) in project.root_package_modules.iter() { + let used_name = if let Some(new_name) = renamed_modules.get(name) { + new_name + } else { + name + }; + output.push_str(&format!( + "-- {used_name}.gleam\n{}\n\n", + modules + .get(name) + .map(|string| string.as_str()) + .unwrap_or(src) + )); + } + output.push_str(&format!( + "-- app.gleam\n{}", + modules + .get("app") + .map(|string| string.as_str()) + .unwrap_or(src) + )); + output +} + macro_rules! assert_rename { ($code:literal, $new_name:literal, $position:expr $(,)?) => { assert_rename!(TestProject::for_source($code), $new_name, $position); @@ -105,30 +175,7 @@ macro_rules! assert_rename { let position = $position.find_position(src); let (range, result) = apply_rename(&project, $new_name, position); - let mut output = String::from("----- BEFORE RENAME\n"); - for (name, src) in project.root_package_modules.iter() { - output.push_str(&format!("-- {name}.gleam\n{src}\n\n")); - } - output.push_str(&format!( - "-- app.gleam\n{}\n\n----- AFTER RENAME\n", - hover::show_hover(src, range, range.start) - )); - for (name, src) in project.root_package_modules.iter() { - output.push_str(&format!( - "-- {name}.gleam\n{}\n\n", - result - .get(*name) - .map(|string| string.as_str()) - .unwrap_or(*src) - )); - } - output.push_str(&format!( - "-- app.gleam\n{}", - result - .get("app") - .map(|string| string.as_str()) - .unwrap_or(src) - )); + let output = display_result(&project, result, HashMap::new(), Some(range)); insta::assert_snapshot!(insta::internals::AutoName, output, src); }; @@ -163,6 +210,18 @@ macro_rules! assert_rename_error { }; } +macro_rules! assert_rename_files { + ($(($old_name:literal, $new_name:literal, $module_src:literal)),+, $code:literal, $(,)?) => { + let project = TestProject::for_source($code)$(.add_module($old_name, $module_src))*; + + let renames = [$(($old_name, $new_name),)*]; + let result = rename_files(&project, &renames); + let output = display_result(&project, result, renames.into(), None); + + insta::assert_snapshot!(insta::internals::AutoName, output, project.src); + }; +} + #[test] fn rename_local_variable() { assert_rename!( @@ -2604,3 +2663,164 @@ pub type Option(anything) { find_position_of("anything") ); } +#[test] +fn renaming_file_modifies_imports_and_references() { + assert_rename_files!( + ( + "wibble/wobble", + "wibble/wubble", + " +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble +" + ), + " +import wibble/wobble.{Wibble} + +pub fn main() -> wobble.Wibble { + assert wobble.wibble == Wibble + wobble.Wobble +} +", + ); +} + +#[test] +fn change_directory_of_file() { + assert_rename_files!( + ( + "wobble", + "wibble/wobble", + " +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble +" + ), + " +import wobble.{Wibble} + +pub fn main() -> wobble.Wibble { + assert wobble.wibble == Wibble + wobble.Wobble +} +", + ); +} + +#[test] +fn rename_file_does_not_modify_aliased_imports() { + assert_rename_files!( + ( + "wibble/wobble", + "wibble/wubble", + " +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble +" + ), + " +import wibble/wobble.{Wibble} as wibble + +pub fn main() -> wibble.Wibble { + assert wibble.wibble == Wibble + wibble.Wobble +} +", + ); +} + +#[test] +fn rename_file_removes_unnecessary_alias() { + assert_rename_files!( + ( + "wibble/wobble", + "wibble/wibble", + " +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble +" + ), + " +import wibble/wobble.{Wibble} as wibble + +pub fn main() -> wibble.Wibble { + assert wibble.wibble == Wibble + wibble.Wobble +} +", + ); +} + +#[test] +fn rename_file_changes_all_correct_ast_nodes() { + assert_rename_files!( + ( + "wibble", + "wobble", + " +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble +" + ), + " +import wibble + +pub const one = wibble.Wibble + +pub const two = wibble.wibble + +pub fn main() -> wibble.Wibble { + case wibble.Wobble { + x if x == wibble.Wibble -> x + x if x == wibble.wibble -> x + wibble.Wobble -> wibble.wibble + } +} +", + ); +} + +#[test] +fn rename_multiple_files() { + assert_rename_files!( + ( + "wibble", + "wibble/wibble", + "pub type Wibble { Wibble Wobble }" + ), + ( + "wobble", + "wibble/wobble", + "import wibble +pub const wibble = wibble.Wobble" + ), + " +import wibble +import wobble + +pub fn main() -> wibble.Wibble { + wobble.wibble +} +", + ); +} diff --git a/language-server/src/tests/snapshots/gleam_language_server__tests__rename__change_directory_of_file.snap b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__change_directory_of_file.snap new file mode 100644 index 000000000..23aaa0d15 --- /dev/null +++ b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__change_directory_of_file.snap @@ -0,0 +1,44 @@ +--- +source: language-server/src/tests/rename.rs +expression: "\nimport wobble.{Wibble}\n\npub fn main() -> wobble.Wibble {\n assert wobble.wibble == Wibble\n wobble.Wobble\n}\n" +--- +----- BEFORE RENAME +-- wobble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wobble.{Wibble} + +pub fn main() -> wobble.Wibble { + assert wobble.wibble == Wibble + wobble.Wobble +} + + +----- AFTER RENAME +-- wibble/wobble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wibble/wobble.{Wibble} + +pub fn main() -> wobble.Wibble { + assert wobble.wibble == Wibble + wobble.Wobble +} diff --git a/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_changes_all_correct_ast_nodes.snap b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_changes_all_correct_ast_nodes.snap new file mode 100644 index 000000000..c362f049e --- /dev/null +++ b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_changes_all_correct_ast_nodes.snap @@ -0,0 +1,58 @@ +--- +source: language-server/src/tests/rename.rs +expression: "\nimport wibble\n\npub const one = wibble.Wibble\n\npub const two = wibble.wibble\n\npub fn main() -> wibble.Wibble {\n case wibble.Wobble {\n x if x == wibble.Wibble -> x\n x if x == wibble.wibble -> x\n wibble.Wobble -> wibble.wibble\n }\n}\n" +--- +----- BEFORE RENAME +-- wibble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wibble + +pub const one = wibble.Wibble + +pub const two = wibble.wibble + +pub fn main() -> wibble.Wibble { + case wibble.Wobble { + x if x == wibble.Wibble -> x + x if x == wibble.wibble -> x + wibble.Wobble -> wibble.wibble + } +} + + +----- AFTER RENAME +-- wobble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wobble + +pub const one = wobble.Wibble + +pub const two = wobble.wibble + +pub fn main() -> wobble.Wibble { + case wobble.Wobble { + x if x == wobble.Wibble -> x + x if x == wobble.wibble -> x + wobble.Wobble -> wobble.wibble + } +} diff --git a/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_does_not_modify_aliased_imports.snap b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_does_not_modify_aliased_imports.snap new file mode 100644 index 000000000..8fec0b347 --- /dev/null +++ b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_does_not_modify_aliased_imports.snap @@ -0,0 +1,44 @@ +--- +source: language-server/src/tests/rename.rs +expression: "\nimport wibble/wobble.{Wibble} as wibble\n\npub fn main() -> wibble.Wibble {\n assert wibble.wibble == Wibble\n wibble.Wobble\n}\n" +--- +----- BEFORE RENAME +-- wibble/wobble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wibble/wobble.{Wibble} as wibble + +pub fn main() -> wibble.Wibble { + assert wibble.wibble == Wibble + wibble.Wobble +} + + +----- AFTER RENAME +-- wibble/wubble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wibble/wubble.{Wibble} as wibble + +pub fn main() -> wibble.Wibble { + assert wibble.wibble == Wibble + wibble.Wobble +} diff --git a/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_removes_unnecessary_alias.snap b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_removes_unnecessary_alias.snap new file mode 100644 index 000000000..55a44c588 --- /dev/null +++ b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_file_removes_unnecessary_alias.snap @@ -0,0 +1,44 @@ +--- +source: language-server/src/tests/rename.rs +expression: "\nimport wibble/wobble.{Wibble} as wibble\n\npub fn main() -> wibble.Wibble {\n assert wibble.wibble == Wibble\n wibble.Wobble\n}\n" +--- +----- BEFORE RENAME +-- wibble/wobble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wibble/wobble.{Wibble} as wibble + +pub fn main() -> wibble.Wibble { + assert wibble.wibble == Wibble + wibble.Wobble +} + + +----- AFTER RENAME +-- wibble/wibble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wibble/wibble.{Wibble} + +pub fn main() -> wibble.Wibble { + assert wibble.wibble == Wibble + wibble.Wobble +} diff --git a/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_multiple_files.snap b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_multiple_files.snap new file mode 100644 index 000000000..efae0704b --- /dev/null +++ b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__rename_multiple_files.snap @@ -0,0 +1,38 @@ +--- +source: language-server/src/tests/rename.rs +expression: "\nimport wibble\nimport wobble\n\npub fn main() -> wibble.Wibble {\n wobble.wibble\n}\n" +--- +----- BEFORE RENAME +-- wibble.gleam +pub type Wibble { Wibble Wobble } + +-- wobble.gleam +import wibble +pub const wibble = wibble.Wobble + +-- app.gleam + +import wibble +import wobble + +pub fn main() -> wibble.Wibble { + wobble.wibble +} + + +----- AFTER RENAME +-- wibble/wibble.gleam +pub type Wibble { Wibble Wobble } + +-- wibble/wobble.gleam +import wibble/wibble +pub const wibble = wibble.Wobble + +-- app.gleam + +import wibble/wibble +import wibble/wobble + +pub fn main() -> wibble.Wibble { + wobble.wibble +} diff --git a/language-server/src/tests/snapshots/gleam_language_server__tests__rename__renaming_file_modifies_imports_and_references.snap b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__renaming_file_modifies_imports_and_references.snap new file mode 100644 index 000000000..96fedd10b --- /dev/null +++ b/language-server/src/tests/snapshots/gleam_language_server__tests__rename__renaming_file_modifies_imports_and_references.snap @@ -0,0 +1,44 @@ +--- +source: language-server/src/tests/rename.rs +expression: "\nimport wibble/wobble.{Wibble}\n\npub fn main() -> wobble.Wibble {\n assert wobble.wibble == Wibble\n wobble.Wobble\n}\n" +--- +----- BEFORE RENAME +-- wibble/wobble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wibble/wobble.{Wibble} + +pub fn main() -> wobble.Wibble { + assert wobble.wibble == Wibble + wobble.Wobble +} + + +----- AFTER RENAME +-- wibble/wubble.gleam + +pub type Wibble { + Wibble + Wobble +} + +pub const wibble = Wibble + + +-- app.gleam + +import wibble/wubble.{Wibble} + +pub fn main() -> wubble.Wibble { + assert wubble.wibble == Wibble + wubble.Wobble +}