diff --git a/CHANGELOG.md b/CHANGELOG.md index 94a346a2cbb..cb383597b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ ## Unreleased +- The language server now offers code actions to wrap a function reference in an + anonymous function, or to remove a trivial anonymous function, leaving its + contents. For example: + + ```gleam + pub fn main() { + [-1, -2, -3] |> list.map(fn(a) { int.absolute_value(a) }) + // ^^ Activating the "Remove anonymous function" + // code action here + } + ``` + + would result in: + + ```gleam + pub fn main() { + [-1, -2, -3] |> list.map(int.absolute_value) + } + ``` + + while the other action would reverse the change. ([Eli Treuherz](http.github.com/treuherz)) + ### Compiler - The compiler now performs function inlining optimisations for a specific set diff --git a/compiler-core/src/ast/typed.rs b/compiler-core/src/ast/typed.rs index 6a36b75fd6e..fbf50d03c3d 100644 --- a/compiler-core/src/ast/typed.rs +++ b/compiler-core/src/ast/typed.rs @@ -76,6 +76,7 @@ pub enum TypedExpr { type_: Arc, fun: Box, arguments: Vec>, + arguments_start: Option, }, BinOp { diff --git a/compiler-core/src/ast/untyped.rs b/compiler-core/src/ast/untyped.rs index 52aabae8bf0..cc85b111b30 100644 --- a/compiler-core/src/ast/untyped.rs +++ b/compiler-core/src/ast/untyped.rs @@ -53,6 +53,7 @@ pub enum UntypedExpr { location: SrcSpan, fun: Box, arguments: Vec>, + arguments_start: u32, }, BinOp { @@ -293,11 +294,18 @@ impl HasLocation for UntypedExpr { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FunctionLiteralKind { Capture { hole: SrcSpan }, - Anonymous { head: SrcSpan }, + Anonymous { head: SrcSpan, body: SrcSpan }, Use { location: SrcSpan }, } impl FunctionLiteralKind { + pub fn is_anonymous(&self) -> bool { + match self { + FunctionLiteralKind::Anonymous { .. } => true, + FunctionLiteralKind::Capture { .. } | FunctionLiteralKind::Use { .. } => false, + } + } + pub fn is_capture(&self) -> bool { match self { FunctionLiteralKind::Capture { .. } => true, diff --git a/compiler-core/src/ast/visit.rs b/compiler-core/src/ast/visit.rs index ae2242fed29..53f9bd522e9 100644 --- a/compiler-core/src/ast/visit.rs +++ b/compiler-core/src/ast/visit.rs @@ -196,8 +196,9 @@ pub trait Visit<'ast> { type_: &'ast Arc, fun: &'ast TypedExpr, arguments: &'ast [TypedCallArg], + arguments_start: &'ast Option, ) { - visit_typed_expr_call(self, location, type_, fun, arguments); + visit_typed_expr_call(self, location, type_, fun, arguments, arguments_start); } fn visit_typed_expr_bin_op( @@ -814,7 +815,8 @@ where type_, fun, arguments, - } => v.visit_typed_expr_call(location, type_, fun, arguments), + arguments_start, + } => v.visit_typed_expr_call(location, type_, fun, arguments, arguments_start), TypedExpr::BinOp { location, type_, @@ -1049,6 +1051,7 @@ pub fn visit_typed_expr_call<'a, V>( _type_: &'a Arc, fun: &'a TypedExpr, arguments: &'a [TypedCallArg], + _arguments_start: &'a Option, ) where V: Visit<'a> + ?Sized, { diff --git a/compiler-core/src/ast_folder.rs b/compiler-core/src/ast_folder.rs index e1340c84812..97e63c4d53e 100644 --- a/compiler-core/src/ast_folder.rs +++ b/compiler-core/src/ast_folder.rs @@ -289,7 +289,8 @@ pub trait UntypedExprFolder: TypeAstFolder + UntypedConstantFolder + PatternFold location, fun, arguments, - } => self.fold_call(location, fun, arguments), + arguments_start, + } => self.fold_call(location, fun, arguments, arguments_start), UntypedExpr::BinOp { location, @@ -445,6 +446,7 @@ pub trait UntypedExprFolder: TypeAstFolder + UntypedConstantFolder + PatternFold location, fun, arguments, + arguments_start, } => { let fun = Box::new(self.fold_expr(*fun)); let arguments = arguments @@ -458,6 +460,7 @@ pub trait UntypedExprFolder: TypeAstFolder + UntypedConstantFolder + PatternFold location, fun, arguments, + arguments_start, } } @@ -770,11 +773,13 @@ pub trait UntypedExprFolder: TypeAstFolder + UntypedConstantFolder + PatternFold location: SrcSpan, fun: Box, arguments: Vec>, + arguments_start: u32, ) -> UntypedExpr { UntypedExpr::Call { location, fun, arguments, + arguments_start, } } diff --git a/compiler-core/src/format.rs b/compiler-core/src/format.rs index 1483f3a9041..452fde331c4 100644 --- a/compiler-core/src/format.rs +++ b/compiler-core/src/format.rs @@ -1592,6 +1592,7 @@ impl<'comments> Formatter<'comments> { fun, arguments, location, + .. })) = call.first() else { // The body of a capture being not a fn shouldn't be possible... diff --git a/compiler-core/src/inline.rs b/compiler-core/src/inline.rs index 97f28bd0853..64e682fd4a3 100644 --- a/compiler-core/src/inline.rs +++ b/compiler-core/src/inline.rs @@ -363,7 +363,8 @@ impl Inliner<'_> { type_, fun, arguments, - } => self.call(location, type_, fun, arguments), + arguments_start, + } => self.call(location, type_, fun, arguments, arguments_start), TypedExpr::BinOp { location, @@ -581,6 +582,7 @@ impl Inliner<'_> { type_: Arc, function: Box, arguments: Vec, + arguments_start: Option, ) -> TypedExpr { let arguments = self.arguments(arguments); @@ -699,6 +701,7 @@ impl Inliner<'_> { type_, fun: Box::new(function), arguments, + arguments_start, } } @@ -1534,6 +1537,7 @@ impl InlinableExpression { .iter() .map(|argument| argument.to_call_arg(Self::to_expression)) .collect(), + arguments_start: None, }, } } diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs index 88eff3127b0..9a0a8d8e6b7 100644 --- a/compiler-core/src/language_server/code_action.rs +++ b/compiler-core/src/language_server/code_action.rs @@ -102,6 +102,28 @@ fn count_indentation(code: &str, line_numbers: &LineNumbers, line: u32) -> usize indent_size } +// Given a string and a position in it, if the position points to whitespace, +// this function returns the next position which doesn't. +fn next_nonwhitespace(string: &EcoString, position: u32) -> u32 { + let mut n = position; + let mut chars = string[position as usize..].chars(); + while chars.next().is_some_and(char::is_whitespace) { + n += 1; + } + n +} + +// Given a string and a position in it, if the position points after whitespace, +// this function returns the previous position which doesn't. +fn previous_nonwhitespace(string: &EcoString, position: u32) -> u32 { + let mut n = position; + let mut chars = string[..position as usize].chars(); + while chars.next_back().is_some_and(char::is_whitespace) { + n -= 1; + } + n +} + /// Code action to remove literal tuples in case subjects, essentially making /// the elements of the tuples into the case's subjects. /// @@ -842,6 +864,7 @@ impl<'ast> ast::visit::Visit<'ast> for FillInMissingLabelledArgs<'ast> { type_: &'ast Arc, fun: &'ast TypedExpr, arguments: &'ast [TypedCallArg], + arguments_start: &'ast Option, ) { let call_range = self.edits.src_span_to_lsp_range(*location); if !within(self.params.range, call_range) { @@ -864,7 +887,7 @@ impl<'ast> ast::visit::Visit<'ast> for FillInMissingLabelledArgs<'ast> { // we're inside a nested call. let previous = self.use_right_hand_side_location; self.use_right_hand_side_location = None; - ast::visit::visit_typed_expr_call(self, location, type_, fun, arguments); + ast::visit::visit_typed_expr_call(self, location, type_, fun, arguments, arguments_start); self.use_right_hand_side_location = previous; } @@ -1263,7 +1286,7 @@ impl<'ast> ast::visit::Visit<'ast> for AddAnnotations<'_> { let location = match kind { // Function captures don't need any type annotations FunctionLiteralKind::Capture { .. } => return, - FunctionLiteralKind::Anonymous { head } => head, + FunctionLiteralKind::Anonymous { head, .. } => head, FunctionLiteralKind::Use { location } => location, }; @@ -3503,11 +3526,13 @@ impl<'ast> ast::visit::Visit<'ast> for ExpandFunctionCapture<'ast> { } } +/// A set of variable names used in some gleam. Useful for passing to [NameGenerator::reserve_variable_names]. struct VariablesNames { names: HashSet, } impl VariablesNames { + /// Creates a `VariableNames` by collecting all variables used in a list of statements. fn from_statements(statements: &[TypedStatement]) -> Self { let mut variables = Self { names: HashSet::new(), @@ -3518,6 +3543,16 @@ impl VariablesNames { } variables } + + /// Creates a `VariableNames` by collecting all variables used within an expression. + fn from_expression(expression: &TypedExpr) -> Self { + let mut variables = Self { + names: HashSet::new(), + }; + + variables.visit_typed_expr(expression); + variables + } } impl<'ast> ast::visit::Visit<'ast> for VariablesNames { @@ -5257,6 +5292,7 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateFunction<'ast> { type_: &'ast Arc, fun: &'ast TypedExpr, arguments: &'ast [TypedCallArg], + arguments_start: &'ast Option, ) { // If the function being called is invalid we need to generate a // function that has the proper labels. @@ -5290,8 +5326,7 @@ impl<'ast> ast::visit::Visit<'ast> for GenerateFunction<'ast> { _ => {} } } - - ast::visit::visit_typed_expr_call(self, location, type_, fun, arguments); + ast::visit::visit_typed_expr_call(self, location, type_, fun, arguments, arguments_start); } } @@ -5614,6 +5649,7 @@ where type_: &'ast Arc, fun: &'ast TypedExpr, arguments: &'ast [TypedCallArg], + arguments_start: &'ast Option, ) { // If the function being called is invalid we need to generate a // function that has the proper labels. @@ -5627,7 +5663,14 @@ where ); } } else { - ast::visit::visit_typed_expr_call(self, location, type_, fun, arguments); + ast::visit::visit_typed_expr_call( + self, + location, + type_, + fun, + arguments, + arguments_start, + ); } } @@ -6090,12 +6133,7 @@ impl<'a> InlineVariable<'a> { } let mut location = assignment.location; - - let mut chars = self.module.code[location.end as usize..].chars(); - // Delete any whitespace after the removed statement - while chars.next().is_some_and(char::is_whitespace) { - location.end += 1; - } + location.end = next_nonwhitespace(&self.module.code, location.end); self.edits.delete(location); @@ -6324,6 +6362,7 @@ impl<'ast> ast::visit::Visit<'ast> for ConvertToPipe<'ast> { _type_: &'ast Arc, fun: &'ast TypedExpr, arguments: &'ast [TypedCallArg], + _arguments_start: &'ast Option, ) { if arguments.iter().any(|arg| arg.is_capture_hole()) { return; @@ -8196,3 +8235,312 @@ impl<'ast> ast::visit::Visit<'ast> for RemoveUnreachableBranches<'ast> { ast::visit::visit_typed_expr_case(self, location, type_, subjects, clauses, compiled_case); } } + +/// Code action to turn a function used as a reference into a one-statement anonymous function. +/// +/// For example, if the code action was used on `op` here: +/// +/// ```gleam +/// list.map([1, 2, 3], op) +/// ``` +/// +/// it would become: +/// +/// ```gleam +/// list.map([1, 2, 3], fn(int) { +/// op(int) +/// }) +/// ``` +pub struct WrapInAnonymousFunction<'a> { + module: &'a Module, + line_numbers: &'a LineNumbers, + params: &'a CodeActionParams, + functions: Vec, +} + +/// Helper struct, a target for [WrapInAnonymousFunction]. +struct FunctionToWrap { + location: SrcSpan, + arguments: Vec>, + variables_names: VariablesNames, +} + +impl<'a> WrapInAnonymousFunction<'a> { + pub fn new( + module: &'a Module, + line_numbers: &'a LineNumbers, + params: &'a CodeActionParams, + ) -> Self { + Self { + module, + line_numbers, + params, + functions: vec![], + } + } + + pub fn code_actions(mut self) -> Vec { + self.visit_typed_module(&self.module.ast); + + let mut actions = Vec::with_capacity(self.functions.len()); + for target in self.functions { + let mut name_generator = NameGenerator::new(); + name_generator.reserve_variable_names(target.variables_names); + let arguments = target + .arguments + .iter() + .map(|t| name_generator.generate_name_from_type(t)) + .join(", "); + + let mut edits = TextEdits::new(self.line_numbers); + edits.insert(target.location.start, format!("fn({arguments}) {{ ")); + edits.insert(target.location.end, format!("({arguments}) }}")); + + CodeActionBuilder::new("Wrap in anonymous function") + .kind(CodeActionKind::REFACTOR_REWRITE) + .changes(self.params.text_document.uri.clone(), edits.edits) + .push_to(&mut actions); + } + actions + } +} + +impl<'ast> ast::visit::Visit<'ast> for WrapInAnonymousFunction<'ast> { + fn visit_typed_expr(&mut self, expression: &'ast TypedExpr) { + let expression_range = src_span_to_lsp_range(expression.location(), self.line_numbers); + if !overlaps(self.params.range, expression_range) { + return; + } + + let is_excluded = match expression { + TypedExpr::Fn { kind, .. } if kind.is_anonymous() => true, + _ => false, + }; + + if let Type::Fn { arguments, .. } = &*expression.type_() + && !is_excluded + { + self.functions.push(FunctionToWrap { + location: expression.location(), + arguments: arguments.clone(), + variables_names: VariablesNames::from_expression(expression), + }); + }; + + ast::visit::visit_typed_expr(self, expression); + } + + /// We don't want to apply to functions that are being explicitly called + /// already, so we need to intercept visits to function calls and bounce + /// them out again so they don't end up in our impl for visit_typed_expr. + /// Otherwise this is the same as []. + fn visit_typed_expr_call( + &mut self, + _location: &'ast SrcSpan, + _type: &'ast Arc, + fun: &'ast TypedExpr, + arguments: &'ast [TypedCallArg], + _arguments_start: &'ast Option, + ) { + // We only need to do this interception for explicit calls, so if any + // of our arguments are explicit we re-enter the visitor as usual. + if arguments.iter().any(|a| a.is_implicit()) { + self.visit_typed_expr(fun); + } else { + // We still want to visit other nodes nested in the function being + // called so we bounce the call back out. + ast::visit::visit_typed_expr(self, fun); + } + + for argument in arguments { + self.visit_typed_call_arg(argument); + } + } +} + +/// Code action to unwrap trivial one-statement anonymous functions into just a +/// reference to the function called +/// +/// For example, if the code action was used on the anonymous function here: +/// +/// ```gleam +/// list.map([1, 2, 3], fn(int) { +/// op(int) +/// }) +/// ``` +/// +/// it would become: +/// +/// ```gleam +/// list.map([1, 2, 3], op) +/// ``` +pub struct UnwrapAnonymousFunction<'a> { + module: &'a Module, + line_numbers: &'a LineNumbers, + params: &'a CodeActionParams, + functions: Vec, +} + +/// Helper struct, a target for [UnwrapAnonymousFunction] +struct FunctionToUnwrap { + /// Location of the anonymous function to apply the action to. + outer_function: SrcSpan, + /// Location of the opening brace of the anonymous function. + outer_function_body_start: u32, + /// Location of the function being called inside the anonymous function. + /// This will be all that's left after the action, plus any comments. + inner_function: SrcSpan, + // Location of the opening parenthesis of the inner function's argument list. + inner_function_arguments_start: u32, +} + +impl<'a> UnwrapAnonymousFunction<'a> { + pub fn new( + module: &'a Module, + line_numbers: &'a LineNumbers, + params: &'a CodeActionParams, + ) -> Self { + Self { + module, + line_numbers, + params, + functions: vec![], + } + } + + pub fn code_actions(mut self) -> Vec { + self.visit_typed_module(&self.module.ast); + + let mut actions = Vec::with_capacity(self.functions.len()); + for function in &self.functions { + let mut edits = TextEdits::new(self.line_numbers); + + // We need to delete the anonymous function's head and the opening + // brace but preserve comments between it and the inner function call. + // We set our endpoint at the start of the function body, and move + // it on through any whitespace. + let head_deletion_end = + next_nonwhitespace(&self.module.code, function.outer_function_body_start + 1); + edits.delete(SrcSpan { + start: function.outer_function.start, + end: head_deletion_end, + }); + + // Delete the inner function call's arguments. + edits.delete(SrcSpan { + start: function.inner_function_arguments_start, + end: function.inner_function.end, + }); + + // To delete the tail we remove the function end (the '}') and any + // whitespace before it. + let tail_deletion_start = + previous_nonwhitespace(&self.module.code, function.outer_function.end - 1); + edits.delete(SrcSpan { + start: tail_deletion_start, + end: function.outer_function.end, + }); + + CodeActionBuilder::new("Remove anonymous function wrapper") + .kind(CodeActionKind::REFACTOR_REWRITE) + .changes(self.params.text_document.uri.clone(), edits.edits) + .push_to(&mut actions); + } + actions + } + + /// If an anonymous function can be unwrapped, save it to our list + /// + /// We need to ensure our subjects: + /// - are anonymous function literals (not captures) + /// - only contain a single statement + /// - that statement is a function call + /// - that call's arguments exactly match the arguments of the enclosing + /// function + fn register_function( + &mut self, + location: &'a SrcSpan, + kind: &'a FunctionLiteralKind, + arguments: &'a [TypedArg], + body: &'a Vec1, + ) { + let outer_body = match kind { + FunctionLiteralKind::Anonymous { body, .. } => body, + _ => return, + }; + + // We can only apply to anonymous functions containing a single function call + let [ + TypedStatement::Expression(TypedExpr::Call { + location: call_location, + arguments: call_arguments, + arguments_start: Some(arguments_start), + .. + }), + ] = body.as_slice() + else { + return; + }; + + // We need the existing argument list for the fn to be a 1:1 match for + // the args we pass to the called function, so we need to collect the + // names used in both lists and check they're equal. + + let outer_argument_names = arguments.iter().map(|a| match &a.names { + ArgNames::Named { name, .. } => Some(name), + // We can bail out early if any arguments are discarded, since + // they couldn't match those actually used. + ArgNames::Discard { .. } => None, + // Anonymous functions can't have labelled arguments. + ArgNames::NamedLabelled { .. } => unreachable!(), + ArgNames::LabelledDiscard { .. } => unreachable!(), + }); + + let inner_argument_names = call_arguments.iter().map(|a| match &a.value { + TypedExpr::Var { name, .. } => Some(name), + // We can bail out early if any of these aren't variables, since + // they couldn't match the inputs. + _ => None, + }); + + if !inner_argument_names.eq(outer_argument_names) { + return; + } + + self.functions.push(FunctionToUnwrap { + outer_function: *location, + outer_function_body_start: outer_body.start, + inner_function: *call_location, + inner_function_arguments_start: *arguments_start, + }) + } +} + +impl<'ast> ast::visit::Visit<'ast> for UnwrapAnonymousFunction<'ast> { + fn visit_typed_expr_fn( + &mut self, + location: &'ast SrcSpan, + type_: &'ast Arc, + kind: &'ast FunctionLiteralKind, + arguments: &'ast [TypedArg], + body: &'ast Vec1, + return_annotation: &'ast Option, + ) { + let function_range = src_span_to_lsp_range(*location, self.line_numbers); + if !overlaps(self.params.range, function_range) { + return; + } + + self.register_function(location, kind, arguments, body); + + ast::visit::visit_typed_expr_fn( + self, + location, + type_, + kind, + arguments, + body, + return_annotation, + ) + } +} diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs index a07835cb4b4..5216520788e 100644 --- a/compiler-core/src/language_server/engine.rs +++ b/compiler-core/src/language_server/engine.rs @@ -48,9 +48,9 @@ use super::{ FillInMissingLabelledArgs, FillUnusedFields, FixBinaryOperation, FixTruncatedBitArraySegment, GenerateDynamicDecoder, GenerateFunction, GenerateJsonEncoder, GenerateVariant, InlineVariable, InterpolateString, LetAssertToCase, PatternMatchOnValue, - RedundantTupleInCaseSubject, RemoveEchos, RemoveUnusedImports, UseLabelShorthandSyntax, - WrapInBlock, code_action_add_missing_patterns, - code_action_convert_qualified_constructor_to_unqualified, + RedundantTupleInCaseSubject, RemoveEchos, RemoveUnusedImports, UnwrapAnonymousFunction, + UseLabelShorthandSyntax, WrapInAnonymousFunction, WrapInBlock, + code_action_add_missing_patterns, code_action_convert_qualified_constructor_to_unqualified, code_action_convert_unqualified_constructor_to_qualified, code_action_import_module, code_action_inexhaustive_let_to_case, }, @@ -447,6 +447,8 @@ where actions.extend(WrapInBlock::new(module, &lines, ¶ms).code_actions()); actions.extend(RemoveBlock::new(module, &lines, ¶ms).code_actions()); actions.extend(RemovePrivateOpaque::new(module, &lines, ¶ms).code_actions()); + actions.extend(WrapInAnonymousFunction::new(module, &lines, ¶ms).code_actions()); + actions.extend(UnwrapAnonymousFunction::new(module, &lines, ¶ms).code_actions()); GenerateDynamicDecoder::new(module, &lines, ¶ms, &mut actions).code_actions(); GenerateJsonEncoder::new( module, diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs index 64554bf878e..0330ae1a806 100644 --- a/compiler-core/src/language_server/tests/action.rs +++ b/compiler-core/src/language_server/tests/action.rs @@ -134,6 +134,8 @@ const REMOVE_BLOCK: &str = "Remove block"; const REMOVE_OPAQUE_FROM_PRIVATE_TYPE: &str = "Remove opaque from private type"; const COLLAPSE_NESTED_CASE: &str = "Collapse nested case"; const REMOVE_UNREACHABLE_BRANCHES: &str = "Remove unreachable branches"; +const WRAP_IN_ANONYMOUS_FUNCTION: &str = "Wrap in anonymous function"; +const UNWRAP_ANONYMOUS_FUNCTION: &str = "Remove anonymous function wrapper"; macro_rules! assert_code_action { ($title:expr, $code:literal, $range:expr $(,)?) => { @@ -9796,3 +9798,439 @@ fn remove_unreachable_branches_does_not_pop_up_if_all_branches_are_reachable() { find_position_of("Ok(n)").to_selection() ); } + +#[test] +fn wrap_uncalled_function_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + op +} + +fn op(i) { + todo +} +", + find_position_of("op").to_selection() + ); +} + +#[test] +fn wrap_uncalled_constructor_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + Record +} + +type Record { + Record(i: Int) +} +", + find_position_of("Record").to_selection() + ); +} + +#[test] +fn wrap_call_arg_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "import gleam/list + +pub fn main() { + list.map([1, 2, 3], op) +} + +fn op(i: Int) -> Int { + todo +} +", + find_position_of("op").to_selection() + ); +} + +#[test] +fn wrap_function_in_anonymous_function_without_shadowing() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + int +} + +fn int(i: Int) { + todo +} +", + find_position_of("int").to_selection() + ); +} + +#[test] +fn wrap_assignment_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + let op = op_factory(1, 2, 3) +} + +fn op_factory(a: Int, b: Int, c: Int) -> fn(Int) -> Int { + todo +} +", + find_position_of("op_factory").to_selection() + ); +} + +#[test] +fn wrap_imported_function_in_anonymous_function() { + let source = "import gleam/list +import gleam/int + +pub fn main() { + list.map([1, 2, 3], int.is_even) +} +"; + + let int_source = "pub fn is_even(int) { int % 2 == 0 }"; + + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + TestProject::for_source(source).add_module("gleam/int", int_source), + find_position_of("int.is_even").to_selection() + ); +} + +#[test] +fn dont_wrap_anonymous_function_in_anonymous_function() { + assert_no_code_actions!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + let f = fn(in) { ception(in) } +} + +", + find_position_of("fn(in)").to_selection() + ); +} + +#[test] +fn wrap_pipeline_step_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + 1 |> wibble |> wobble +} + +fn wibble(i) { + todo +} + +fn wobble(i) { + todo +} + +", + find_position_of("wibble").to_selection() + ); +} + +#[test] +fn wrap_multiargument_pipeline_step_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + 1 |> wibble(2) |> wobble +} + +fn wibble(a, b) { + todo +} + +fn wobble(i) { + todo +} + +", + find_position_of("wibble").to_selection() + ); +} + +#[test] +fn wrap_capturing_pipeline_step_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + 1 |> wibble(2, _) |> wobble +} + +fn wibble(a, b) { + todo +} + +fn wobble(i) { + todo +} + +", + find_position_of("wibble").to_selection() + ); +} + +#[test] +fn wrap_final_pipeline_step_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + 1 |> wibble |> wobble +} + +fn wibble(i) { + todo +} + +fn wobble(i) { + todo +} + +", + find_position_of("wobble").to_selection() + ); +} + +#[test] +fn wrap_record_field_in_anonymous_function() { + assert_code_action!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + let r = Record(wibble) +} + +type Record { + Record(wibbler: fn(Int) -> Int) +} + +fn wibble(v) { + todo +} + +", + find_position_of("wibble").to_selection() + ); +} + +#[test] +fn dont_wrap_functions_that_are_already_being_called() { + assert_no_code_actions!( + WRAP_IN_ANONYMOUS_FUNCTION, + "pub fn main() { + wibble(1) +} + +fn wibble(i) { + todo +} + +", + find_position_of("wibble").to_selection() + ); +} + +#[test] +fn unwrap_trivial_anonymous_function() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "import gleam/list + +pub fn main() { + list.map([1, 2, 3], fn(int) { op(int) }) +} + +fn op(i: Int) -> Int { + todo +} +", + find_position_of("fn(int)").to_selection() + ); +} + +#[test] +fn unwrap_nested_anonymous_function() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "pub fn main() { + fn(do) { fn(re){ mi(re) }(do) } +} + +fn mi(v) { + todo +} +", + find_position_of("fn(re)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_with_labelled_args() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "pub fn main() { + fn(a, b) { op(first: a, second: b) } +} + +fn op(first a, second b) { + todo +} +", + find_position_of("fn(a, b)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_with_labelled_and_unlabelled_args() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "pub fn main() { + fn(a, b, c) { op(a, second: b, third: c) } +} + +fn op(a, second b, third c) { + todo +} +", + find_position_of("fn(a, b, c)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_with_labelled_args_out_of_order() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "pub fn main() { + fn(a, b) { op(second: b, first: a) } +} + +fn op(first a, second b) { + todo +} +", + find_position_of("fn(a, b)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_with_comment_after() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "pub fn main() { + fn(a) { + op(a) + // look out! + } +} + +fn op(a) { + todo +} +", + find_position_of("fn(a)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_with_comment_on_line() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "pub fn main() { + fn(a) { + op(a) // look out! + } +} + +fn op(a) { + todo +} +", + find_position_of("fn(a)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_with_comment_on_head_line() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "pub fn main() { + fn(a) { // look out! + op(a) + } +} + +fn op(a) { + todo +} +", + find_position_of("fn(a)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_with_comments_before() { + assert_code_action!( + UNWRAP_ANONYMOUS_FUNCTION, + "pub fn main() { + fn(a) { + // look out, + // there's a comment! + + // another comment! + //here's one without a leading space + // here's one indented wrong + // here's one indented even wronger + op(a) + } +} + +fn op(a) { + todo +} +", + find_position_of("fn(a)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_unavailable_when_args_discarded() { + assert_no_code_actions!( + UNWRAP_ANONYMOUS_FUNCTION, + "import gleam/list + +pub fn main() { + list.index_map([1, 2, 3], fn(_, int) { op(int) }) +} + +fn op(i: Int) -> Int { + todo +} +", + find_position_of("fn(_, int)").to_selection() + ); +} + +#[test] +fn unwrap_anonymous_function_unavailable_with_different_args() { + assert_no_code_actions!( + UNWRAP_ANONYMOUS_FUNCTION, + "import gleam/list + +const another_int = 7 + +pub fn main() { + list.map([1, 2, 3], fn(int) { op(another_int) }) +} + +fn op(i: Int) -> Int { + todo +} +", + find_position_of("fn(int)").to_selection() + ); +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_after.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_after.snap new file mode 100644 index 00000000000..9182e6f8bc3 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_after.snap @@ -0,0 +1,27 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n fn(a) {\n op(a)\n // look out!\n }\n}\n\nfn op(a) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + fn(a) { + ↑ + op(a) + // look out! + } +} + +fn op(a) { + todo +} + + +----- AFTER ACTION +pub fn main() { + op + // look out! +} + +fn op(a) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_on_head_line.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_on_head_line.snap new file mode 100644 index 00000000000..3ff44c0f5e8 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_on_head_line.snap @@ -0,0 +1,26 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n fn(a) { // look out!\n op(a)\n }\n}\n\nfn op(a) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + fn(a) { // look out! + ↑ + op(a) + } +} + +fn op(a) { + todo +} + + +----- AFTER ACTION +pub fn main() { + // look out! + op +} + +fn op(a) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_on_line.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_on_line.snap new file mode 100644 index 00000000000..20dd999d126 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comment_on_line.snap @@ -0,0 +1,25 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n fn(a) {\n op(a) // look out!\n }\n}\n\nfn op(a) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + fn(a) { + ↑ + op(a) // look out! + } +} + +fn op(a) { + todo +} + + +----- AFTER ACTION +pub fn main() { + op // look out! +} + +fn op(a) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comments_before.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comments_before.snap new file mode 100644 index 00000000000..a3581ce647e --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_comments_before.snap @@ -0,0 +1,39 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n fn(a) {\n // look out,\n // there's a comment!\n\n // another comment!\n //here's one without a leading space\n // here's one indented wrong\n // here's one indented even wronger\n op(a)\n }\n}\n\nfn op(a) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + fn(a) { + ↑ + // look out, + // there's a comment! + + // another comment! + //here's one without a leading space + // here's one indented wrong + // here's one indented even wronger + op(a) + } +} + +fn op(a) { + todo +} + + +----- AFTER ACTION +pub fn main() { + // look out, + // there's a comment! + + // another comment! + //here's one without a leading space + // here's one indented wrong + // here's one indented even wronger + op +} + +fn op(a) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_and_unlabelled_args.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_and_unlabelled_args.snap new file mode 100644 index 00000000000..ce7877a29b6 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_and_unlabelled_args.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n fn(a, b, c) { op(a, second: b, third: c) }\n}\n\nfn op(a, second b, third c) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + fn(a, b, c) { op(a, second: b, third: c) } + ↑ +} + +fn op(a, second b, third c) { + todo +} + + +----- AFTER ACTION +pub fn main() { + op +} + +fn op(a, second b, third c) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_args.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_args.snap new file mode 100644 index 00000000000..8efeb9fce57 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_args.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n fn(a, b) { op(first: a, second: b) }\n}\n\nfn op(first a, second b) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + fn(a, b) { op(first: a, second: b) } + ↑ +} + +fn op(first a, second b) { + todo +} + + +----- AFTER ACTION +pub fn main() { + op +} + +fn op(first a, second b) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_args_out_of_order.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_args_out_of_order.snap new file mode 100644 index 00000000000..cd8d164e85c --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_anonymous_function_with_labelled_args_out_of_order.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n fn(a, b) { op(second: b, first: a) }\n}\n\nfn op(first a, second b) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + fn(a, b) { op(second: b, first: a) } + ↑ +} + +fn op(first a, second b) { + todo +} + + +----- AFTER ACTION +pub fn main() { + op +} + +fn op(first a, second b) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_nested_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_nested_anonymous_function.snap new file mode 100644 index 00000000000..fda471bce94 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_nested_anonymous_function.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n fn(do) { fn(re){ mi(re) }(do) }\n}\n\nfn mi(v) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + fn(do) { fn(re){ mi(re) }(do) } + ↑ +} + +fn mi(v) { + todo +} + + +----- AFTER ACTION +pub fn main() { + fn(do) { mi(do) } +} + +fn mi(v) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_trivial_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_trivial_anonymous_function.snap new file mode 100644 index 00000000000..17dde9b1c49 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__unwrap_trivial_anonymous_function.snap @@ -0,0 +1,27 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "import gleam/list\n\npub fn main() {\n list.map([1, 2, 3], fn(int) { op(int) })\n}\n\nfn op(i: Int) -> Int {\n todo\n}\n" +--- +----- BEFORE ACTION +import gleam/list + +pub fn main() { + list.map([1, 2, 3], fn(int) { op(int) }) + ↑ +} + +fn op(i: Int) -> Int { + todo +} + + +----- AFTER ACTION +import gleam/list + +pub fn main() { + list.map([1, 2, 3], op) +} + +fn op(i: Int) -> Int { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_anonymous_function_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_anonymous_function_in_anonymous_function.snap new file mode 100644 index 00000000000..2eca67b09de --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_anonymous_function_in_anonymous_function.snap @@ -0,0 +1,16 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n let f = fn(in) { ception(in) }\n}\n\n" +--- +----- BEFORE ACTION +pub fn main() { + let f = fn(in) { ception(in) } + ↑ +} + + + +----- AFTER ACTION +pub fn main() { + let f = fn(value) { fn(in) { ception(in) }(value) } +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_assignment_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_assignment_in_anonymous_function.snap new file mode 100644 index 00000000000..7c5f9f89f46 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_assignment_in_anonymous_function.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n let op = op_factory(1, 2, 3)\n}\n\nfn op_factory(a: Int, b: Int, c: Int) -> fn(Int) -> Int {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + let op = op_factory(1, 2, 3) + ↑ +} + +fn op_factory(a: Int, b: Int, c: Int) -> fn(Int) -> Int { + todo +} + + +----- AFTER ACTION +pub fn main() { + let op = fn(int) { op_factory(1, 2, 3)(int) } +} + +fn op_factory(a: Int, b: Int, c: Int) -> fn(Int) -> Int { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_call_arg_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_call_arg_in_anonymous_function.snap new file mode 100644 index 00000000000..3eeb6c7524a --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_call_arg_in_anonymous_function.snap @@ -0,0 +1,27 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "import gleam/list\n\npub fn main() {\n list.map([1, 2, 3], op)\n}\n\nfn op(i: Int) -> Int {\n todo\n}\n" +--- +----- BEFORE ACTION +import gleam/list + +pub fn main() { + list.map([1, 2, 3], op) + ↑ +} + +fn op(i: Int) -> Int { + todo +} + + +----- AFTER ACTION +import gleam/list + +pub fn main() { + list.map([1, 2, 3], fn(int) { op(int) }) +} + +fn op(i: Int) -> Int { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_capturing_pipeline_step_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_capturing_pipeline_step_in_anonymous_function.snap new file mode 100644 index 00000000000..b43fc178fc9 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_capturing_pipeline_step_in_anonymous_function.snap @@ -0,0 +1,32 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n 1 |> wibble(2, _) |> wobble\n}\n\nfn wibble(a, b) {\n todo\n}\n\nfn wobble(i) {\n todo\n}\n\n" +--- +----- BEFORE ACTION +pub fn main() { + 1 |> wibble(2, _) |> wobble + ↑ +} + +fn wibble(a, b) { + todo +} + +fn wobble(i) { + todo +} + + + +----- AFTER ACTION +pub fn main() { + 1 |> fn(int) { wibble(2, _)(int) } |> wobble +} + +fn wibble(a, b) { + todo +} + +fn wobble(i) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_final_pipeline_step_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_final_pipeline_step_in_anonymous_function.snap new file mode 100644 index 00000000000..8ed2822fcbb --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_final_pipeline_step_in_anonymous_function.snap @@ -0,0 +1,32 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n 1 |> wibble |> wobble\n}\n\nfn wibble(i) {\n todo\n}\n\nfn wobble(i) {\n todo\n}\n\n" +--- +----- BEFORE ACTION +pub fn main() { + 1 |> wibble |> wobble + ↑ +} + +fn wibble(i) { + todo +} + +fn wobble(i) { + todo +} + + + +----- AFTER ACTION +pub fn main() { + 1 |> wibble |> fn(value) { wobble(value) } +} + +fn wibble(i) { + todo +} + +fn wobble(i) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_function_in_anonymous_function_without_shadowing.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_function_in_anonymous_function_without_shadowing.snap new file mode 100644 index 00000000000..ce1baa53f0f --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_function_in_anonymous_function_without_shadowing.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n int\n}\n\nfn int(i: Int) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + int + ↑ +} + +fn int(i: Int) { + todo +} + + +----- AFTER ACTION +pub fn main() { + fn(int_2) { int(int_2) } +} + +fn int(i: Int) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_imported_function_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_imported_function_in_anonymous_function.snap new file mode 100644 index 00000000000..90bd8b656ee --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_imported_function_in_anonymous_function.snap @@ -0,0 +1,21 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "import gleam/list\nimport gleam/int\n\npub fn main() {\n list.map([1, 2, 3], int.is_even)\n}\n" +--- +----- BEFORE ACTION +import gleam/list +import gleam/int + +pub fn main() { + list.map([1, 2, 3], int.is_even) + ↑ +} + + +----- AFTER ACTION +import gleam/list +import gleam/int + +pub fn main() { + list.map([1, 2, 3], fn(int) { int.is_even(int) }) +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_in_anonymous_function_works_when_nested.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_in_anonymous_function_works_when_nested.snap new file mode 100644 index 00000000000..3c0b38a7042 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_in_anonymous_function_works_when_nested.snap @@ -0,0 +1,33 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n [1, 2, 3]\n |> wibble(wobble, _)\n}\n\nfn wibble(f, i) {\n todo\n}\n\nfn wobble(int) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + [1, 2, 3] + |> wibble(wobble, _) + ↑ +} + +fn wibble(f, i) { + todo +} + +fn wobble(int) { + todo +} + + +----- AFTER ACTION +pub fn main() { + [1, 2, 3] + |> wibble(fn(value) { wobble(value) }, _) +} + +fn wibble(f, i) { + todo +} + +fn wobble(int) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_multiargument_pipeline_step_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_multiargument_pipeline_step_in_anonymous_function.snap new file mode 100644 index 00000000000..9ba8be671df --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_multiargument_pipeline_step_in_anonymous_function.snap @@ -0,0 +1,32 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n 1 |> wibble(2) |> wobble\n}\n\nfn wibble(a, b) {\n todo\n}\n\nfn wobble(i) {\n todo\n}\n\n" +--- +----- BEFORE ACTION +pub fn main() { + 1 |> wibble(2) |> wobble + ↑ +} + +fn wibble(a, b) { + todo +} + +fn wobble(i) { + todo +} + + + +----- AFTER ACTION +pub fn main() { + 1 |> fn(int, int_2) { wibble(int, int_2) }(2) |> wobble +} + +fn wibble(a, b) { + todo +} + +fn wobble(i) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_pipeline_step_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_pipeline_step_in_anonymous_function.snap new file mode 100644 index 00000000000..8c57b7f6715 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_pipeline_step_in_anonymous_function.snap @@ -0,0 +1,32 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n 1 |> wibble |> wobble\n}\n\nfn wibble(i) {\n todo\n}\n\nfn wobble(i) {\n todo\n}\n\n" +--- +----- BEFORE ACTION +pub fn main() { + 1 |> wibble |> wobble + ↑ +} + +fn wibble(i) { + todo +} + +fn wobble(i) { + todo +} + + + +----- AFTER ACTION +pub fn main() { + 1 |> fn(int) { wibble(int) } |> wobble +} + +fn wibble(i) { + todo +} + +fn wobble(i) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_record_field_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_record_field_in_anonymous_function.snap new file mode 100644 index 00000000000..b9feed9010e --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_record_field_in_anonymous_function.snap @@ -0,0 +1,32 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n let r = Record(wibble)\n}\n\ntype Record {\n Record(wibbler: fn(Int) -> Int)\n}\n\nfn wibble(v) {\n todo\n}\n\n" +--- +----- BEFORE ACTION +pub fn main() { + let r = Record(wibble) + ↑ +} + +type Record { + Record(wibbler: fn(Int) -> Int) +} + +fn wibble(v) { + todo +} + + + +----- AFTER ACTION +pub fn main() { + let r = Record(fn(int) { wibble(int) }) +} + +type Record { + Record(wibbler: fn(Int) -> Int) +} + +fn wibble(v) { + todo +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_uncalled_constructor_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_uncalled_constructor_in_anonymous_function.snap new file mode 100644 index 00000000000..48594ead699 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_uncalled_constructor_in_anonymous_function.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n Record\n}\n\ntype Record {\n Record(i: Int)\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + Record + ↑ +} + +type Record { + Record(i: Int) +} + + +----- AFTER ACTION +pub fn main() { + fn(int) { Record(int) } +} + +type Record { + Record(i: Int) +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_uncalled_function_in_anonymous_function.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_uncalled_function_in_anonymous_function.snap new file mode 100644 index 00000000000..4d54ff7f099 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__wrap_uncalled_function_in_anonymous_function.snap @@ -0,0 +1,23 @@ +--- +source: compiler-core/src/language_server/tests/action.rs +expression: "pub fn main() {\n op\n}\n\nfn op(i) {\n todo\n}\n" +--- +----- BEFORE ACTION +pub fn main() { + op + ↑ +} + +fn op(i) { + todo +} + + +----- AFTER ACTION +pub fn main() { + fn(value) { op(value) } +} + +fn op(i) { + todo +} diff --git a/compiler-core/src/parse.rs b/compiler-core/src/parse.rs index 517ee5710ff..bb2a1e49d72 100644 --- a/compiler-core/src/parse.rs +++ b/compiler-core/src/parse.rs @@ -737,12 +737,19 @@ where arguments, body, return_annotation, + body_start: Some(body_start), end_position, .. })) => UntypedExpr::Fn { location: SrcSpan::new(location.start, end_position), end_of_head_byte_index: location.end, - kind: FunctionLiteralKind::Anonymous { head: location }, + kind: FunctionLiteralKind::Anonymous { + head: location, + body: SrcSpan { + start: body_start, + end: end_position, + }, + }, arguments, body, return_annotation, @@ -933,7 +940,7 @@ where } } _ => { - if self.maybe_one(&Token::LeftParen).is_some() { + if let Some((left_paren, _)) = self.maybe_one(&Token::LeftParen) { let start = expr.location().start; match self.maybe_one(&Token::DotDot) { Some((dot_s, _)) => { @@ -968,7 +975,7 @@ where // Call let arguments = self.parse_fn_arguments()?; let (_, end) = self.expect_one(&Token::RightParen)?; - expr = make_call(expr, arguments, start, end)?; + expr = make_call(expr, arguments, start, end, left_paren)?; } } } else { @@ -4575,6 +4582,7 @@ pub fn make_call( arguments: Vec, start: u32, end: u32, + arguments_start: u32, ) -> Result { let mut hole_location = None; @@ -4621,6 +4629,7 @@ pub fn make_call( location: SrcSpan { start, end }, fun: Box::new(fun), arguments, + arguments_start, }; match hole_location { diff --git a/compiler-core/src/parse/extra.rs b/compiler-core/src/parse/extra.rs index 8f693ff4cc8..2931fe8b68c 100644 --- a/compiler-core/src/parse/extra.rs +++ b/compiler-core/src/parse/extra.rs @@ -40,19 +40,34 @@ impl ModuleExtra { self.first_comment_between(start, end).is_some() } + /// Returns the first comment overlapping the given source locations (inclusive) + /// Note that the returned span covers the text of the comment, not the `//` pub(crate) fn first_comment_between(&self, start: u32, end: u32) -> Option { - self.comments - .binary_search_by(|comment| { - if comment.end < start { - Ordering::Less - } else if comment.start > end { - Ordering::Greater - } else { - Ordering::Equal - } - }) - .ok() - .and_then(|index| self.comments.get(index).copied()) + let inner = |comments: &[SrcSpan], start, end| { + if comments.is_empty() { + return None; + } + + comments + .binary_search_by(|comment| { + if comment.end < start { + Ordering::Less + } else if comment.start > end { + Ordering::Greater + } else { + Ordering::Equal + } + }) + .ok() + }; + + let mut best = None; + let mut search_list = &self.comments[..]; + while let Some(index) = inner(search_list, start, end) { + best = self.comments.get(index); + search_list = search_list.get(0..index).unwrap_or(&[]); + } + best.copied() } } @@ -81,3 +96,69 @@ impl<'a> From<(&SrcSpan, &'a str)> for Comment<'a> { } } } + +#[cfg(test)] +mod tests { + use crate::{ast::SrcSpan, parse::extra::ModuleExtra}; + + fn set_up_extra() -> ModuleExtra { + let mut extra = ModuleExtra::new(); + extra.comments = vec![ + SrcSpan { start: 0, end: 10 }, + SrcSpan { start: 20, end: 30 }, + SrcSpan { start: 40, end: 50 }, + SrcSpan { start: 60, end: 70 }, + SrcSpan { start: 80, end: 90 }, + SrcSpan { + start: 90, + end: 100, + }, + ]; + extra + } + + #[test] + fn first_comment_between() { + let extra = set_up_extra(); + assert!(matches!( + extra.first_comment_between(15, 85), + Some(SrcSpan { start: 20, end: 30 }) + )); + } + + #[test] + fn first_comment_between_equal_to_range() { + let extra = set_up_extra(); + assert!(matches!( + extra.first_comment_between(40, 50), + Some(SrcSpan { start: 40, end: 50 }) + )); + } + + #[test] + fn first_comment_between_overlapping_start_of_range() { + let extra = set_up_extra(); + assert!(matches!( + extra.first_comment_between(45, 80), + Some(SrcSpan { start: 40, end: 50 }) + )); + } + + #[test] + fn first_comment_between_overlapping_end_of_range() { + let extra = set_up_extra(); + assert!(matches!( + extra.first_comment_between(35, 45), + Some(SrcSpan { start: 40, end: 50 }) + )); + } + + #[test] + fn first_comment_between_at_end_of_range() { + let extra = set_up_extra(); + assert!(matches!( + dbg!(extra.first_comment_between(55, 60)), + Some(SrcSpan { start: 60, end: 70 }) + )); + } +} diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuple_access_after_function.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuple_access_after_function.snap index 2382accddf4..9f06f63d973 100644 --- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuple_access_after_function.snap +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__nested_tuple_access_after_function.snap @@ -29,6 +29,7 @@ expression: tuple().0.1 name: "tuple", }, arguments: [], + arguments_start: 5, }, }, }, diff --git a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__record_access_no_label.snap b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__record_access_no_label.snap index 9407b0de2af..1a661b20436 100644 --- a/compiler-core/src/parse/snapshots/gleam_core__parse__tests__record_access_no_label.snap +++ b/compiler-core/src/parse/snapshots/gleam_core__parse__tests__record_access_no_label.snap @@ -142,6 +142,7 @@ Parsed { implicit: None, }, ], + arguments_start: 67, }, }, ), diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs index 4239fdb3aed..4a3afd9fd54 100644 --- a/compiler-core/src/type_/expression.rs +++ b/compiler-core/src/type_/expression.rs @@ -505,8 +505,15 @@ impl<'a, 'b> ExprTyper<'a, 'b> { location, fun, arguments, + arguments_start, .. - } => Ok(self.infer_call(*fun, arguments, location, CallKind::Function)), + } => Ok(self.infer_call( + *fun, + arguments, + location, + Some(arguments_start), + CallKind::Function, + )), UntypedExpr::BinOp { location, @@ -855,6 +862,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { *call.function, call.arguments, call_location, + None, CallKind::Use { call_location: use_call_location, assignments_location: use_.assignments_location, @@ -1148,6 +1156,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { fun: UntypedExpr, arguments: Vec>, location: SrcSpan, + arguments_start: Option, kind: CallKind, ) -> TypedExpr { let (fun, arguments, type_) = self.do_infer_call(fun, arguments, location, kind); @@ -1184,6 +1193,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { location, type_, arguments, + arguments_start, fun: Box::new(fun), } } diff --git a/compiler-core/src/type_/pipe.rs b/compiler-core/src/type_/pipe.rs index 91e73076f01..4486dbb529e 100644 --- a/compiler-core/src/type_/pipe.rs +++ b/compiler-core/src/type_/pipe.rs @@ -128,6 +128,7 @@ impl<'a, 'b, 'c> PipeTyper<'a, 'b, 'c> { arguments, type_: return_type, fun: Box::new(func), + arguments_start: None, }, ) } @@ -300,6 +301,7 @@ impl<'a, 'b, 'c> PipeTyper<'a, 'b, 'c> { type_, arguments, fun: Box::new(function), + arguments_start: None, }; let arguments = vec![self.untyped_left_hand_value_variable_call_argument()]; // TODO: use `.with_unify_error_situation(UnifyErrorSituation::PipeTypeMismatch)` @@ -318,6 +320,7 @@ impl<'a, 'b, 'c> PipeTyper<'a, 'b, 'c> { type_, arguments, fun: Box::new(function), + arguments_start: None, } } @@ -345,6 +348,7 @@ impl<'a, 'b, 'c> PipeTyper<'a, 'b, 'c> { type_, arguments, fun: Box::new(fun), + arguments_start: None, } } @@ -395,6 +399,7 @@ impl<'a, 'b, 'c> PipeTyper<'a, 'b, 'c> { type_: return_type, fun: function, arguments: vec![self.typed_left_hand_value_variable_call_argument()], + arguments_start: None, } }