Repository: gleam-lang/gleam (rust)

Issue:
Extract variable code action
An action that extracts a highlighted value and replaces it with a variable
```gleam
pub fn main() {
  list.map([1, 2, 3], run)
  #        ^^^^^^^^^
}
```

```gleam
pub fn main() {
  let value = [1, 2, 3]
  list.map(value, run)
}
```

Pull request description:
Extract into variable code action
This PR closes #3934 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 084ff403c..4c2ed9b8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,8 @@
 
 ### Compiler
 
-- Removed compiler hint about pattern matching a `Result(a, b)` when being used where `a` is expected.
+- Removed compiler hint about pattern matching a `Result(a, b)` when being used
+  where `a` is expected.
   ([Kieran O'Reilly](https://github.com/SoTeKie))
 
 - Optimised code generated for record updates.
@@ -32,10 +33,10 @@
 
 - The shorthand names for the two targets, `erl` and `js` are now
   deprecated in code such as `@target`.
-
   ([Surya Rose](https://github.com/GearsDatapacks))
 
-- A custom panic message can now be specified when asserting a value with `let assert`:
+- A custom panic message can now be specified when asserting a value with
+  `let assert`:
 
   ```gleam
   let assert Ok(regex) = regex.compile("ab?c+") as "This regex is always valid"
@@ -73,7 +74,8 @@
   O(1) operation instead of O(N), significantly improving performance.
   ([Richard Viney](https://github.com/richard-viney))
 
-- Better error message for existed type constructor being used as value constructor.
+- Better error message for existed type constructor being used as value
+  constructor.
   ([Jiangda Wang](https://github.com/Frank-III))
 
 ### Build tool
@@ -90,11 +92,13 @@
   team packages and v0 packages.
   ([Louis Pilfold](https://github.com/lpil))
 
-- `gleam publish` now warns when publishing packages that define multiple top-level
-  modules, as this can lead to namespace pollution and conflicts for consumers.
+- `gleam publish` now warns when publishing packages that define multiple
+  top-level modules, as this can lead to namespace pollution and conflicts for
+  consumers.
   ([Aleksei Gurianov](https://github.com/guria))
 
 - New projects now require `gleam_stdlib` v0.44.0.
+  ([Louis Pilfold](https://github.com/lpil))
 
 - `gleam remove` no longer requires a network connection.
   ([yoshi](https://github.com/joshi-monster))
@@ -108,7 +112,6 @@
 
 - The language server now provides type information when hovering over argument
   labels.
-
   ([Surya Rose](https://github.com/GearsDatapacks))
 
 - The Language Server now suggests a code action to desugar a use expression
@@ -157,6 +160,7 @@
 
 - The language server now provides correct information when hovering over
   patterns in use expressions.
+  ([Surya Rose](https://github.com/GearsDatapacks))
 
 - The language server now suggests a code action to convert an inexhaustive
   `let` assignment into a `case` expression:
@@ -182,6 +186,9 @@
 
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- The language server now provides an action to extract a value into a variable.
+  ([Giacomo Cavalieri](https://github.com/giacomocavalieri))
+
 ### Formatter
 
 - The formatter now adds a `todo` inside empty blocks.
@@ -189,9 +196,9 @@
 
 ### Documentation
 
-- Canonical links created for documentation pages if published on Hex. Previously
-  published documentation would need to be updated. Resolves versioned pages to point
-  to latest page for search engines.
+- Canonical links created for documentation pages if published on Hex.
+  Previously published documentation would need to be updated.
+  Resolves versioned pages to point to latest page for search engines.
   ([Dave Lage](https://github.com/rockerBOO))
 
 - The formatter now format lists the same in constants as in expressions
@@ -210,18 +217,20 @@
   subjects and one of them being a constant record.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
-- Variant inference on prelude types now works correctly if the variant is constant.
+- Variant inference on prelude types now works correctly if the variant is
+  constant.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
-- Fixed a bug where patterns in `use` expressions would not be checked to ensure that
-  they were exhaustive.
+- Fixed a bug where patterns in `use` expressions would not be checked to ensure
+  that they were exhaustive.
   ([Surya Rose](https://github.com/GearsDatapacks))
 
 - Fixed a bug where a `module.mjs` file would be overwritten by a `module.gleam`
   file of same name without warning. It now produces an error.
   ([PgBiel](https://github.com/PgBiel))
 
-- Modules depending on removed or renamed modules now get automatically recompiled.
+- Modules depending on removed or renamed modules now get automatically
+  recompiled.
   ([Sakari Bergen](https://github.com/sbergen))
 
 - The compiler now raises a warning for unused case expressions, code blocks and
diff --git a/README.md b/README.md
index 1f2d33ffb..713131683 100644
--- a/README.md
+++ b/README.md
@@ -262,6 +262,7 @@ Thank you to our sponsors! Gleam would not be possible without you.
   <a href="https://github.com/maxmcd">Max McDonnell</a> -
   <a href="https://github.com/max-tern">max-tern</a> -
   <a href="https://github.com/metame">metame</a> -
+  <a href="https://github.com/metatexx">METATEXX GmbH</a> -
   <a href="https://github.com/amiroff">Metin Emiroğlu</a> -
   <a href="https://github.com/stunthamster">Michael Duffy</a> -
   <a href="https://github.com/michaeljones">Michael Jones</a> -
diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs
index 85a948342..33eaa7439 100644
--- a/compiler-core/src/language_server/code_action.rs
+++ b/compiler-core/src/language_server/code_action.rs
@@ -3,9 +3,7 @@ use std::{iter, sync::Arc};
 use crate::{
     ast::{
         self,
-        visit::{
-            visit_typed_call_arg, visit_typed_expr_call, visit_typed_pattern_call_arg, Visit as _,
-        },
+        visit::{visit_typed_call_arg, visit_typed_pattern_call_arg, Visit as _},
         AssignName, AssignmentKind, CallArg, FunctionLiteralKind, ImplicitCallArgOrigin, Pattern,
         SrcSpan, TypedAssignment, TypedExpr, TypedModuleConstant, TypedPattern, TypedStatement,
         TypedUse,
@@ -807,7 +805,7 @@ impl<'ast> ast::visit::Visit<'ast> for FillInMissingLabelledArgs<'ast> {
         // containing the current selection so we can't stop at the first call
         // we find (the outermost one) and have to keep traversing it in case
         // we're inside a nested call.
-        visit_typed_expr_call(self, location, type_, fun, args)
+        ast::visit::visit_typed_expr_call(self, location, type_, fun, args)
     }
 }
 
@@ -2600,3 +2598,134 @@ fn turn_expression_into_use(expr: &TypedExpr) -> Option<CallLocations> {
         callback_body_span,
     })
 }
+
+/// Builder for code action to apply the turn into use expression.
+///
+pub struct ExtractVariable<'a> {
+    module: &'a Module,
+    params: &'a CodeActionParams,
+    edits: TextEdits<'a>,
+    inside_capture_body: bool,
+    selected_expression: Option<SrcSpan>,
+    statement_before_selected_expression: Option<SrcSpan>,
+    latest_statement: Option<SrcSpan>,
+}
+
+impl<'a> ExtractVariable<'a> {
+    pub fn new(
+        module: &'a Module,
+        line_numbers: &'a LineNumbers,
+        params: &'a CodeActionParams,
+    ) -> Self {
+        Self {
+            module,
+            params,
+            edits: TextEdits::new(line_numbers),
+            inside_capture_body: false,
+            selected_expression: None,
+            latest_statement: None,
+            statement_before_selected_expression: None,
+        }
+    }
+
+    pub fn code_actions(mut self) -> Vec<CodeAction> {
+        self.visit_typed_module(&self.module.ast);
+
+        let Some(location) = self.selected_expression else {
+            return vec![];
+        };
+
+        if let Some(container_location) = self.statement_before_selected_expression {
+            let nesting = self
+                .edits
+                .src_span_to_lsp_range(container_location)
+                .start
+                .character;
+            let nesting = " ".repeat(nesting as usize);
+            let content = self
+                .module
+                .code
+                .get(location.start as usize..location.end as usize)
+                .expect("selected expression");
+            self.edits.insert(
+                container_location.start,
+                format!("let value = {content}\n{nesting}"),
+            );
+        }
+        self.edits.replace(location, "value".into());
+
+        let mut action = Vec::with_capacity(1);
+        CodeActionBuilder::new("Extract variable")
+            .kind(CodeActionKind::REFACTOR_REWRITE)
+            .changes(self.params.text_document.uri.clone(), self.edits.edits)
+            .preferred(false)
+            .push_to(&mut action);
+        action
+    }
+}
+
+impl<'ast> ast::visit::Visit<'ast> for ExtractVariable<'ast> {
+    fn visit_typed_statement(&mut self, stmt: &'ast TypedStatement) {
+        // A capture body is comprised of just a single expression statement
+        // that is inserted by the compiler, we don't really want to put
+        // anything before that; so in this case we avoid tracking it.
+        if !self.inside_capture_body {
+            self.latest_statement = Some(stmt.location());
+        }
+        ast::visit::visit_typed_statement(self, stmt);
+    }
+
+    fn visit_typed_expr(&mut self, expr: &'ast TypedExpr) {
+        let expr_location = expr.location();
+        let expr_range = self.edits.src_span_to_lsp_range(expr_location);
+        if within(self.params.range, expr_range) {
+            self.selected_expression = Some(expr_location);
+            self.statement_before_selected_expression = self.latest_statement;
+        }
+
+        ast::visit::visit_typed_expr(self, expr);
+    }
+
+    fn visit_typed_expr_fn(
+        &mut self,
+        location: &'ast SrcSpan,
+        type_: &'ast Arc<Type>,
+        kind: &'ast FunctionLiteralKind,
+        args: &'ast [ast::TypedArg],
+        body: &'ast [TypedStatement],
+        return_annotation: &'ast Option<ast::TypeAst>,
+    ) {
+        self.inside_capture_body = match kind {
+            // If a fn is a capture `int.wibble(1, _)` its body will consist of
+            // just a single expression statement. When visiting we must record
+            // we're inside a capture body.
+            FunctionLiteralKind::Capture => true,
+            FunctionLiteralKind::Anonymous { .. } | FunctionLiteralKind::Use { .. } => false,
+        };
+        ast::visit::visit_typed_expr_fn(self, location, type_, kind, args, body, return_annotation);
+        self.inside_capture_body = false;
+    }
+
+    // We don't want to offer the action if the cursor is over a variable
+    // already!
+    fn visit_typed_expr_var(
+        &mut self,
+        location: &'ast SrcSpan,
+        _constructor: &'ast type_::ValueConstructor,
+        _name: &'ast EcoString,
+    ) {
+        let var_range = self.edits.src_span_to_lsp_range(*location);
+        if within(self.params.range, var_range) {
+            self.selected_expression = None;
+        }
+    }
+
+    // We don't want to offer the action if the cursor is over some invalid
+    // piece of code.
+    fn visit_typed_expr_invalid(&mut self, location: &'ast SrcSpan, _type_: &'ast Arc<Type>) {
+        let invalid_range = self.edits.src_span_to_lsp_range(*location);
+        if within(self.params.range, invalid_range) {
+            self.selected_expression = None;
+        }
+    }
+}
diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs
index b25c8e9b1..fc6a3f8cf 100644
--- a/compiler-core/src/language_server/engine.rs
+++ b/compiler-core/src/language_server/engine.rs
@@ -33,7 +33,7 @@ use super::{
         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, AddAnnotations, CodeActionBuilder, DesugarUse,
-        FillInMissingLabelledArgs, LabelShorthandSyntax, LetAssertToCase,
+        ExtractVariable, FillInMissingLabelledArgs, LabelShorthandSyntax, LetAssertToCase,
         RedundantTupleInCaseSubject, TurnIntoUse,
     },
     completer::Completer,
@@ -332,6 +332,7 @@ where
             actions.extend(FillInMissingLabelledArgs::new(module, &lines, &params).code_actions());
             actions.extend(DesugarUse::new(module, &lines, &params).code_actions());
             actions.extend(TurnIntoUse::new(module, &lines, &params).code_actions());
+            actions.extend(ExtractVariable::new(module, &lines, &params).code_actions());
             AddAnnotations::new(module, &lines, &params).code_action(&mut actions);
             Ok(if actions.is_empty() {
                 None


Test patch:
diff --git a/compiler-core/src/language_server/tests/action.rs b/compiler-core/src/language_server/tests/action.rs
index 4f83edbaa..5e8bd50b6 100644
--- a/compiler-core/src/language_server/tests/action.rs
+++ b/compiler-core/src/language_server/tests/action.rs
@@ -68,6 +68,7 @@ const ADD_ANNOTATION: &str = "Add type annotation";
 const ADD_ANNOTATIONS: &str = "Add type annotations";
 const DESUGAR_USE_EXPRESSION: &str = "Convert from `use`";
 const CONVERT_TO_USE: &str = "Convert to `use`";
+const EXTRACT_VARIABLE: &str = "Extract variable";
 
 macro_rules! assert_code_action {
     ($title:expr, $code:literal, $range:expr $(,)?) => {
@@ -3790,3 +3791,171 @@ fn no_code_action_for_exhaustive_let_to_case() {
         find_position_of("let").select_until(find_position_of("=")),
     );
 }
+
+#[test]
+fn extract_variable() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+  list.map([1, 2, 3], int.add(1, _))
+}"#,
+        find_position_of("[1").select_until(find_position_of("2"))
+    );
+}
+
+#[test]
+fn extract_variable_does_not_extract_a_variable() {
+    assert_no_code_actions!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+    let z = 1
+    let a = [1, 2, z]
+}"#,
+        find_position_of("z").nth_occurrence(2).to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_2() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+  list.map([1, 2, 3], int.add(1, _))
+}"#,
+        find_position_of("int.").select_until(find_position_of("add"))
+    );
+}
+
+#[test]
+fn extract_variable_from_capture_arguments() {
+    assert_no_code_actions!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+  int.add(1, _)
+}"#,
+        find_position_of("_").to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_from_capture_arguments_2() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+  int.add(11, _)
+}"#,
+        find_position_of("11").to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_3() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+  list.map([1, 2, 3], todo, todo)
+}"#,
+        find_position_of("todo")
+            .nth_occurrence(2)
+            .select_until(find_position_of("todo)").under_last_char())
+    );
+}
+
+#[test]
+fn extract_variable_inside_multiline_function_call() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+  list.map(
+    [1, 2, 3],
+    int.add(1, _),
+  )
+}"#,
+        find_position_of("[1").to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_in_case_branch() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+    case wibble {
+      _ -> [1, 2, 3]
+    }
+}"#,
+        find_position_of("[1").to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_in_multiline_case_subject_branch() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+    case
+        list.map(
+          [1, 2, 3],
+          int.add(1, _)
+        )
+    {
+      _ -> todo
+    }
+}"#,
+        find_position_of("[1").to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_in_use() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+    use <- wibble([1, 2, 3])
+    todo
+}"#,
+        find_position_of("[1").to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_inside_use_body() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+    use <- wibble(todo)
+    list.map([1, 2, 3], int.add(1, _))
+    todo
+}"#,
+        find_position_of("[1").to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_in_multiline_use() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+    use <- wibble(
+        [1, 2, 3]
+    )
+    todo
+}"#,
+        find_position_of("[1").to_selection()
+    );
+}
+
+#[test]
+fn extract_variable_in_block() {
+    assert_code_action!(
+        EXTRACT_VARIABLE,
+        r#"pub fn main() {
+  {
+    todo
+    wibble([1, 2, 3])
+    todo
+  }
+}"#,
+        find_position_of("2").select_until(find_position_of("3"))
+    );
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable.snap
new file mode 100644
index 000000000..1041d0e0a
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable.snap
@@ -0,0 +1,16 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n  list.map([1, 2, 3], int.add(1, _))\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+  list.map([1, 2, 3], int.add(1, _))
+           ▔▔▔▔↑                    
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+  let value = [1, 2, 3]
+  list.map(value, int.add(1, _))
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_2.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_2.snap
new file mode 100644
index 000000000..b7cc29bed
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_2.snap
@@ -0,0 +1,16 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n  list.map([1, 2, 3], int.add(1, _))\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+  list.map([1, 2, 3], int.add(1, _))
+                      ▔▔▔▔↑         
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+  let value = int.add(1, _)
+  list.map([1, 2, 3], value)
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_3.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_3.snap
new file mode 100644
index 000000000..f8b94ce4f
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_3.snap
@@ -0,0 +1,16 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n  list.map([1, 2, 3], todo, todo)\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+  list.map([1, 2, 3], todo, todo)
+                            ▔▔▔▔↑
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+  let value = todo
+  list.map([1, 2, 3], todo, value)
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_from_capture_arguments_2.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_from_capture_arguments_2.snap
new file mode 100644
index 000000000..4a3a57ba2
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_from_capture_arguments_2.snap
@@ -0,0 +1,16 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n  int.add(11, _)\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+  int.add(11, _)
+          ↑     
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+  let value = 11
+  int.add(value, _)
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_block.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_block.snap
new file mode 100644
index 000000000..e01d6b53b
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_block.snap
@@ -0,0 +1,24 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n  {\n    todo\n    wibble([1, 2, 3])\n    todo\n  }\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+  {
+    todo
+    wibble([1, 2, 3])
+               ▔▔▔↑  
+    todo
+  }
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+  {
+    todo
+    let value = [1, 2, 3]
+    wibble(value)
+    todo
+  }
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_case_branch.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_case_branch.snap
new file mode 100644
index 000000000..817f721d2
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_case_branch.snap
@@ -0,0 +1,20 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n    case wibble {\n      _ -> [1, 2, 3]\n    }\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+    case wibble {
+      _ -> [1, 2, 3]
+           ↑        
+    }
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+    let value = [1, 2, 3]
+    case wibble {
+      _ -> value
+    }
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_multiline_case_subject_branch.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_multiline_case_subject_branch.snap
new file mode 100644
index 000000000..6cfe6461c
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_multiline_case_subject_branch.snap
@@ -0,0 +1,30 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n    case\n        list.map(\n          [1, 2, 3],\n          int.add(1, _)\n        )\n    {\n      _ -> todo\n    }\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+    case
+        list.map(
+          [1, 2, 3],
+          ↑         
+          int.add(1, _)
+        )
+    {
+      _ -> todo
+    }
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+    let value = [1, 2, 3]
+    case
+        list.map(
+          value,
+          int.add(1, _)
+        )
+    {
+      _ -> todo
+    }
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_multiline_use.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_multiline_use.snap
new file mode 100644
index 000000000..a36d4de45
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_multiline_use.snap
@@ -0,0 +1,22 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n    use <- wibble(\n        [1, 2, 3]\n    )\n    todo\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+    use <- wibble(
+        [1, 2, 3]
+        ↑        
+    )
+    todo
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+    let value = [1, 2, 3]
+    use <- wibble(
+        value
+    )
+    todo
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_use.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_use.snap
new file mode 100644
index 000000000..d9b2d603c
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_in_use.snap
@@ -0,0 +1,18 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n    use <- wibble([1, 2, 3])\n    todo\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+    use <- wibble([1, 2, 3])
+                  ↑         
+    todo
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+    let value = [1, 2, 3]
+    use <- wibble(value)
+    todo
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_inside_multiline_function_call.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_inside_multiline_function_call.snap
new file mode 100644
index 000000000..0d527a320
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_inside_multiline_function_call.snap
@@ -0,0 +1,22 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n  list.map(\n    [1, 2, 3],\n    int.add(1, _),\n  )\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+  list.map(
+    [1, 2, 3],
+    ↑         
+    int.add(1, _),
+  )
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+  let value = [1, 2, 3]
+  list.map(
+    value,
+    int.add(1, _),
+  )
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_inside_use_body.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_inside_use_body.snap
new file mode 100644
index 000000000..c729c06b9
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__extract_variable_inside_use_body.snap
@@ -0,0 +1,20 @@
+---
+source: compiler-core/src/language_server/tests/action.rs
+expression: "pub fn main() {\n    use <- wibble(todo)\n    list.map([1, 2, 3], int.add(1, _))\n    todo\n}"
+---
+----- BEFORE ACTION
+pub fn main() {
+    use <- wibble(todo)
+    list.map([1, 2, 3], int.add(1, _))
+             ↑                        
+    todo
+}
+
+
+----- AFTER ACTION
+pub fn main() {
+    use <- wibble(todo)
+    let value = [1, 2, 3]
+    list.map(value, int.add(1, _))
+    todo
+}