languages: Add inline values support for JavaScript, TypeScript, and TSX (#40914)

phpjit and Anthony created

Adds debugger inline values support for JavaScript, TypeScript, and TSX languages. 

Release Notes:

- debugger: Add inline value support for Javascript, TypeScript, and TSX

---------

Co-authored-by: Anthony <anthony@zed.dev>

Change summary

crates/debugger_ui/src/tests/inline_values.rs | 259 ++++++++++++++++++++
crates/languages/src/javascript/debugger.scm  |  23 +
crates/languages/src/tsx/debugger.scm         |  25 ++
crates/languages/src/typescript/debugger.scm  |  23 +
4 files changed, 329 insertions(+), 1 deletion(-)

Detailed changes

crates/debugger_ui/src/tests/inline_values.rs 🔗

@@ -3,7 +3,10 @@ use std::{path::Path, sync::Arc};
 use dap::{Scope, StackFrame, Variable, requests::Variables};
 use editor::{Editor, EditorMode, MultiBuffer};
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
-use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_python, tree_sitter_rust};
+use language::{
+    Language, LanguageConfig, LanguageMatcher, tree_sitter_python, tree_sitter_rust,
+    tree_sitter_typescript,
+};
 use project::{FakeFs, Project};
 use serde_json::json;
 use unindent::Unindent as _;
@@ -2272,3 +2275,257 @@ fn main() {
     )
     .await;
 }
+
+fn javascript_lang() -> Language {
+    let debug_variables_query = include_str!("../../../languages/src/javascript/debugger.scm");
+    Language::new(
+        LanguageConfig {
+            name: "JavaScript".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["js".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
+    )
+    .with_debug_variables_query(debug_variables_query)
+    .unwrap()
+}
+
+fn typescript_lang() -> Language {
+    let debug_variables_query = include_str!("../../../languages/src/typescript/debugger.scm");
+    Language::new(
+        LanguageConfig {
+            name: "TypeScript".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["ts".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
+    )
+    .with_debug_variables_query(debug_variables_query)
+    .unwrap()
+}
+
+fn tsx_lang() -> Language {
+    let debug_variables_query = include_str!("../../../languages/src/tsx/debugger.scm");
+    Language::new(
+        LanguageConfig {
+            name: "TSX".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["tsx".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
+        Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
+    )
+    .with_debug_variables_query(debug_variables_query)
+    .unwrap()
+}
+
+#[gpui::test]
+async fn test_javascript_inline_values(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+    let variables = [
+        ("x", "10"),
+        ("y", "20"),
+        ("sum", "30"),
+        ("message", "Hello"),
+    ];
+
+    let before = r#"
+function calculate() {
+    const x = 10;
+    const y = 20;
+    const sum = x + y;
+    const message = "Hello";
+    console.log(message, "Sum:", sum);
+}
+"#
+    .unindent();
+
+    let after = r#"
+function calculate() {
+    const x: 10 = 10;
+    const y: 20 = 20;
+    const sum: 30 = x: 10 + y: 20;
+    const message: Hello = "Hello";
+    console.log(message, "Sum:", sum);
+}
+"#
+    .unindent();
+
+    test_inline_values_util(
+        &variables,
+        &[],
+        &before,
+        &after,
+        None,
+        javascript_lang(),
+        executor,
+        cx,
+    )
+    .await;
+}
+
+#[gpui::test]
+async fn test_typescript_inline_values(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+    let variables = [
+        ("count", "42"),
+        ("name", "Alice"),
+        ("result", "84"),
+        ("i", "3"),
+    ];
+
+    let before = r#"
+function processData(count: number, name: string): number {
+    let result = count * 2;
+    for (let i = 0; i < 5; i++) {
+        console.log(i);
+    }
+    return result;
+}
+"#
+    .unindent();
+
+    let after = r#"
+function processData(count: number, name: string): number {
+    let result: 84 = count: 42 * 2;
+    for (let i: 3 = 0; i: 3 < 5; i: 3++) {
+        console.log(i);
+    }
+    return result: 84;
+}
+"#
+    .unindent();
+
+    test_inline_values_util(
+        &variables,
+        &[],
+        &before,
+        &after,
+        None,
+        typescript_lang(),
+        executor,
+        cx,
+    )
+    .await;
+}
+
+#[gpui::test]
+async fn test_tsx_inline_values(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+    let variables = [("count", "5"), ("message", "Hello React")];
+
+    let before = r#"
+const Counter = () => {
+    const count = 5;
+    const message = "Hello React";
+    return (
+        <div>
+            <p>{message}</p>
+            <span>{count}</span>
+        </div>
+    );
+};
+"#
+    .unindent();
+
+    let after = r#"
+const Counter = () => {
+    const count: 5 = 5;
+    const message: Hello React = "Hello React";
+    return (
+        <div>
+            <p>{message: Hello React}</p>
+            <span>{count}</span>
+        </div>
+    );
+};
+"#
+    .unindent();
+
+    test_inline_values_util(
+        &variables,
+        &[],
+        &before,
+        &after,
+        None,
+        tsx_lang(),
+        executor,
+        cx,
+    )
+    .await;
+}
+
+#[gpui::test]
+async fn test_javascript_arrow_functions(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+    let variables = [("x", "42"), ("result", "84")];
+
+    let before = r#"
+const double = (x) => {
+    const result = x * 2;
+    return result;
+};
+"#
+    .unindent();
+
+    let after = r#"
+const double = (x) => {
+    const result: 84 = x: 42 * 2;
+    return result: 84;
+};
+"#
+    .unindent();
+
+    test_inline_values_util(
+        &variables,
+        &[],
+        &before,
+        &after,
+        None,
+        javascript_lang(),
+        executor,
+        cx,
+    )
+    .await;
+}
+
+#[gpui::test]
+async fn test_typescript_for_in_loop(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+    let variables = [("key", "name"), ("obj", "{name: 'test'}")];
+
+    let before = r#"
+function iterate() {
+    const obj = {name: 'test'};
+    for (const key in obj) {
+        console.log(key);
+    }
+}
+"#
+    .unindent();
+
+    let after = r#"
+function iterate() {
+    const obj: {name: 'test'} = {name: 'test'};
+    for (const key: name in obj) {
+        console.log(key);
+    }
+}
+"#
+    .unindent();
+
+    test_inline_values_util(
+        &variables,
+        &[],
+        &before,
+        &after,
+        None,
+        typescript_lang(),
+        executor,
+        cx,
+    )
+    .await;
+}

crates/languages/src/javascript/debugger.scm 🔗

@@ -0,0 +1,23 @@
+(lexical_declaration (variable_declarator name: (identifier) @debug-variable))
+
+(for_in_statement left: (identifier) @debug-variable)
+(for_statement initializer: (lexical_declaration (variable_declarator name: (identifier) @debug-variable)))
+
+(binary_expression left: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+(binary_expression right: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(unary_expression argument: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+(update_expression argument: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(return_statement (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(parenthesized_expression (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(array (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(pair value: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(member_expression object: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(statement_block) @debug-scope
+(program) @debug-scope

crates/languages/src/tsx/debugger.scm 🔗

@@ -0,0 +1,25 @@
+(lexical_declaration (variable_declarator name: (identifier) @debug-variable))
+
+(for_in_statement left: (identifier) @debug-variable)
+(for_statement initializer: (lexical_declaration (variable_declarator name: (identifier) @debug-variable)))
+
+(binary_expression left: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+(binary_expression right: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(unary_expression argument: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+(update_expression argument: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(return_statement (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(parenthesized_expression (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(jsx_expression (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(array (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(pair value: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(member_expression object: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(statement_block) @debug-scope
+(program) @debug-scope

crates/languages/src/typescript/debugger.scm 🔗

@@ -0,0 +1,23 @@
+(lexical_declaration (variable_declarator name: (identifier) @debug-variable))
+
+(for_in_statement left: (identifier) @debug-variable)
+(for_statement initializer: (lexical_declaration (variable_declarator name: (identifier) @debug-variable)))
+
+(binary_expression left: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+(binary_expression right: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(unary_expression argument: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+(update_expression argument: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(return_statement (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(parenthesized_expression (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(array (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(pair value: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(member_expression object: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
+
+(statement_block) @debug-scope
+(program) @debug-scope