Repository: gleam-lang/gleam (rust)

Issue:
Hovering for argument labels
Currently, hovering on the label of an argument shows the type of the whole function/pattern. I would expect it to behave the same as hovering over the argument value itself.

For example:
```gleam
fn add(wibble a, wobble b) {
  a + b
}

fn main() {
  add(wibble: 1, wobble: 2)
  //          ^ Hovering here correctly shows `Int`
  add(wibble: 1, wobble: 2)
  //  ^ Hovering here shows `fn(Int, Int) -> Int`, but it should probably also show `Int`
}
```

I already have a mostly working implementation for this but I thought I'd make an issue first

Pull request description:
Hovering for argument labels
Closes #3849 
This PR implements hovering for argument labels.
I wasn't sure whether this is considered a bug fix or a new feature, but other similar changes were put under bug fixes so I did the same for this.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4639880dd..96b720635 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,10 @@
 
 ### Language Server
 
+- The language server now provides type information when hovering over argument
+  labels.
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ### Formatter
 
 ### Bug fixed
diff --git a/compiler-core/src/ast.rs b/compiler-core/src/ast.rs
index efcff40e3..faa436197 100644
--- a/compiler-core/src/ast.rs
+++ b/compiler-core/src/ast.rs
@@ -1248,14 +1248,28 @@ impl CallArg<TypedExpr> {
                 .or_else(|| body.iter().find_map(|s| s.find_node(byte_index))),
             // In all other cases we're happy with the default behaviour.
             //
-            _ => self.value.find_node(byte_index),
+            _ => {
+                if let Some(located) = self.value.find_node(byte_index) {
+                    Some(located)
+                } else if self.location.contains(byte_index) && self.label.is_some() {
+                    Some(Located::Label(self.location, self.value.type_()))
+                } else {
+                    None
+                }
+            }
         }
     }
 }
 
 impl CallArg<TypedPattern> {
     pub fn find_node(&self, byte_index: u32) -> Option<Located<'_>> {
-        self.value.find_node(byte_index)
+        if let Some(located) = self.value.find_node(byte_index) {
+            Some(located)
+        } else if self.location.contains(byte_index) && self.label.is_some() {
+            Some(Located::Label(self.location, self.value.type_()))
+        } else {
+            None
+        }
     }
 }
 
diff --git a/compiler-core/src/build.rs b/compiler-core/src/build.rs
index 73124f23a..ae60115ca 100644
--- a/compiler-core/src/build.rs
+++ b/compiler-core/src/build.rs
@@ -340,6 +340,7 @@ pub enum Located<'a> {
     Arg(&'a TypedArg),
     Annotation(SrcSpan, std::sync::Arc<Type>),
     UnqualifiedImport(UnqualifiedImport<'a>),
+    Label(SrcSpan, std::sync::Arc<Type>),
 }
 
 impl<'a> Located<'a> {
@@ -393,6 +394,7 @@ impl<'a> Located<'a> {
             }),
             Self::Arg(_) => None,
             Self::Annotation(_, type_) => self.type_location(importable_modules, type_.clone()),
+            Self::Label(_, _) => None,
         }
     }
 }
diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs
index de3188abc..1fdfc2529 100644
--- a/compiler-core/src/language_server/engine.rs
+++ b/compiler-core/src/language_server/engine.rs
@@ -280,6 +280,8 @@ where
                 Located::Arg(_) => None,
 
                 Located::Annotation(_, _) => Some(completer.completion_types()),
+
+                Located::Label(_, _) => None,
             };
 
             Ok(completions)
@@ -597,6 +599,9 @@ Unused labelled fields:
                         module,
                     ))
                 }
+                Located::Label(location, type_) => {
+                    Some(hover_for_label(location, type_, lines, module))
+                }
             })
         })
     }
@@ -866,6 +871,20 @@ fn hover_for_annotation(
     }
 }
 
+fn hover_for_label(
+    location: SrcSpan,
+    type_: Arc<Type>,
+    line_numbers: LineNumbers,
+    module: &Module,
+) -> Hover {
+    let type_ = Printer::new(&module.ast.names).print_type(&type_);
+    let contents = format!("```gleam\n{type_}\n```");
+    Hover {
+        contents: HoverContents::Scalar(MarkedString::String(contents)),
+        range: Some(src_span_to_lsp_range(location, &line_numbers)),
+    }
+}
+
 fn hover_for_module_constant(
     constant: &ModuleConstant<Arc<Type>, EcoString>,
     line_numbers: LineNumbers,


Test patch:
diff --git a/compiler-core/src/language_server/tests/hover.rs b/compiler-core/src/language_server/tests/hover.rs
index 998968461..278e29966 100644
--- a/compiler-core/src/language_server/tests/hover.rs
+++ b/compiler-core/src/language_server/tests/hover.rs
@@ -1274,3 +1274,40 @@ pub fn main() {
         find_position_of("w: Wibble").under_char('b')
     );
 }
+
+#[test]
+fn hover_for_label_in_pattern() {
+    let code = "
+type Wibble {
+  Wibble(wibble: Int, wobble: Int)
+}
+
+pub fn main() {
+  let Wibble(wibble: _, wobble: _) = todo
+  todo
+}
+";
+
+    assert_hover!(
+        TestProject::for_source(code),
+        find_position_of("wibble: _").under_char('l')
+    );
+}
+
+#[test]
+fn hover_for_label_in_expression() {
+    let code = "
+fn add(wibble a, wobble b) {
+  a + b
+}
+
+pub fn main() {
+  add(wibble: 1, wobble: 2)
+}
+";
+
+    assert_hover!(
+        TestProject::for_source(code),
+        find_position_of("wibble:").under_char('i')
+    );
+}
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_for_label_in_expression.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_for_label_in_expression.snap
new file mode 100644
index 000000000..16a94cecf
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_for_label_in_expression.snap
@@ -0,0 +1,20 @@
+---
+source: compiler-core/src/language_server/tests/hover.rs
+expression: "\nfn add(wibble a, wobble b) {\n  a + b\n}\n\npub fn main() {\n  add(wibble: 1, wobble: 2)\n}\n"
+---
+fn add(wibble a, wobble b) {
+  a + b
+}
+
+pub fn main() {
+  add(wibble: 1, wobble: 2)
+      ▔↑▔▔▔▔▔▔▔            
+}
+
+
+----- Hover content -----
+Scalar(
+    String(
+        "```gleam\nInt\n```",
+    ),
+)
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_for_label_in_pattern.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_for_label_in_pattern.snap
new file mode 100644
index 000000000..1511d34d2
--- /dev/null
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__hover__hover_for_label_in_pattern.snap
@@ -0,0 +1,21 @@
+---
+source: compiler-core/src/language_server/tests/hover.rs
+expression: "\ntype Wibble {\n  Wibble(wibble: Int, wobble: Int)\n}\n\npub fn main() {\n  let Wibble(wibble: _, wobble: _) = todo\n  todo\n}\n"
+---
+type Wibble {
+  Wibble(wibble: Int, wobble: Int)
+}
+
+pub fn main() {
+  let Wibble(wibble: _, wobble: _) = todo
+             ▔▔▔▔↑▔▔▔▔                   
+  todo
+}
+
+
+----- Hover content -----
+Scalar(
+    String(
+        "```gleam\nInt\n```",
+    ),
+)