debugger: Show language icons in debug scenario picker (#30662)

Anthony Eid created

We attempt to resolve the language name in this order

1. Based on debug adapter if they're for a singular language e.g. Delve
2. File extension if it exists
3. If a language name exists within a debug scenario's label

In the future I want to use locators to also determine the language as
well and refresh scenario list when a new scenario has been saved

Release Notes:

- N/A

Change summary

crates/dap/src/adapters.rs                  |  7 ++
crates/dap/src/registry.rs                  |  6 ++
crates/dap_adapters/src/go.rs               |  7 ++
crates/dap_adapters/src/php.rs              |  7 ++
crates/dap_adapters/src/python.rs           |  7 ++
crates/dap_adapters/src/ruby.rs             |  7 ++
crates/debugger_ui/src/new_session_modal.rs | 66 +++++++++++++++++++---
crates/language/src/language_registry.rs    | 22 +++++++
8 files changed, 115 insertions(+), 14 deletions(-)

Detailed changes

crates/dap/src/adapters.rs 🔗

@@ -8,7 +8,7 @@ pub use dap_types::{StartDebuggingRequestArguments, StartDebuggingRequestArgumen
 use futures::io::BufReader;
 use gpui::{AsyncApp, SharedString};
 pub use http_client::{HttpClient, github::latest_github_release};
-use language::LanguageToolchainStore;
+use language::{LanguageName, LanguageToolchainStore};
 use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
 use settings::WorktreeId;
@@ -418,6 +418,11 @@ pub trait DebugAdapter: 'static + Send + Sync {
         user_installed_path: Option<PathBuf>,
         cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary>;
+
+    /// Returns the language name of an adapter if it only supports one language
+    fn adapter_language_name(&self) -> Option<LanguageName> {
+        None
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/dap/src/registry.rs 🔗

@@ -2,6 +2,7 @@ use anyhow::Result;
 use async_trait::async_trait;
 use collections::FxHashMap;
 use gpui::{App, Global, SharedString};
+use language::LanguageName;
 use parking_lot::RwLock;
 use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
 
@@ -59,6 +60,11 @@ impl DapRegistry {
         );
     }
 
+    pub fn adapter_language(&self, adapter_name: &str) -> Option<LanguageName> {
+        self.adapter(adapter_name)
+            .and_then(|adapter| adapter.adapter_language_name())
+    }
+
     pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
         let _previous_value = self.0.write().locators.insert(locator.name(), locator);
         debug_assert!(

crates/dap_adapters/src/go.rs 🔗

@@ -1,5 +1,6 @@
 use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
-use gpui::AsyncApp;
+use gpui::{AsyncApp, SharedString};
+use language::LanguageName;
 use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
 
 use crate::*;
@@ -43,6 +44,10 @@ impl DebugAdapter for GoDebugAdapter {
         DebugAdapterName(Self::ADAPTER_NAME.into())
     }
 
+    fn adapter_language_name(&self) -> Option<LanguageName> {
+        Some(SharedString::new_static("Go").into())
+    }
+
     async fn get_binary(
         &self,
         delegate: &dyn DapDelegate,

crates/dap_adapters/src/php.rs 🔗

@@ -1,6 +1,7 @@
 use adapters::latest_github_release;
 use dap::adapters::{DebugTaskDefinition, TcpArguments};
-use gpui::AsyncApp;
+use gpui::{AsyncApp, SharedString};
+use language::LanguageName;
 use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
 use util::ResultExt;
 
@@ -119,6 +120,10 @@ impl DebugAdapter for PhpDebugAdapter {
         DebugAdapterName(Self::ADAPTER_NAME.into())
     }
 
+    fn adapter_language_name(&self) -> Option<LanguageName> {
+        Some(SharedString::new_static("PHP").into())
+    }
+
     async fn get_binary(
         &self,
         delegate: &dyn DapDelegate,

crates/dap_adapters/src/python.rs 🔗

@@ -1,6 +1,7 @@
 use crate::*;
 use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
-use gpui::AsyncApp;
+use gpui::{AsyncApp, SharedString};
+use language::LanguageName;
 use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
 use util::ResultExt;
 
@@ -165,6 +166,10 @@ impl DebugAdapter for PythonDebugAdapter {
         DebugAdapterName(Self::ADAPTER_NAME.into())
     }
 
+    fn adapter_language_name(&self) -> Option<LanguageName> {
+        Some(SharedString::new_static("Python").into())
+    }
+
     async fn get_binary(
         &self,
         delegate: &dyn DapDelegate,

crates/dap_adapters/src/ruby.rs 🔗

@@ -6,7 +6,8 @@ use dap::{
         self, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
     },
 };
-use gpui::AsyncApp;
+use gpui::{AsyncApp, SharedString};
+use language::LanguageName;
 use std::path::PathBuf;
 use util::command::new_smol_command;
 
@@ -25,6 +26,10 @@ impl DebugAdapter for RubyDebugAdapter {
         DebugAdapterName(Self::ADAPTER_NAME.into())
     }
 
+    fn adapter_language_name(&self) -> Option<LanguageName> {
+        Some(SharedString::new_static("Ruby").into())
+    }
+
     async fn get_binary(
         &self,
         delegate: &dyn DapDelegate,

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -1,8 +1,10 @@
 use collections::FxHashMap;
+use language::LanguageRegistry;
 use std::{
     borrow::Cow,
     ops::Not,
     path::{Path, PathBuf},
+    sync::Arc,
     time::Duration,
     usize,
 };
@@ -81,6 +83,7 @@ impl NewSessionModal {
             return;
         };
         let task_store = workspace.project().read(cx).task_store().clone();
+        let languages = workspace.app_state().languages.clone();
 
         cx.spawn_in(window, async move |workspace, cx| {
             workspace.update_in(cx, |workspace, window, cx| {
@@ -131,9 +134,12 @@ impl NewSessionModal {
                                 }
 
                                 this.launch_picker.update(cx, |picker, cx| {
-                                    picker
-                                        .delegate
-                                        .task_contexts_loaded(task_contexts, window, cx);
+                                    picker.delegate.task_contexts_loaded(
+                                        task_contexts,
+                                        languages,
+                                        window,
+                                        cx,
+                                    );
                                     picker.refresh(window, cx);
                                     cx.notify();
                                 });
@@ -944,9 +950,49 @@ impl DebugScenarioDelegate {
         }
     }
 
+    fn get_scenario_kind(
+        languages: &Arc<LanguageRegistry>,
+        dap_registry: &DapRegistry,
+        scenario: DebugScenario,
+    ) -> (Option<TaskSourceKind>, DebugScenario) {
+        let language_names = languages.language_names();
+        let language = dap_registry
+            .adapter_language(&scenario.adapter)
+            .map(|language| TaskSourceKind::Language {
+                name: language.into(),
+            });
+
+        let language = language.or_else(|| {
+            scenario
+                .request
+                .as_ref()
+                .and_then(|request| match request {
+                    DebugRequest::Launch(launch) => launch
+                        .program
+                        .rsplit_once(".")
+                        .and_then(|split| languages.language_name_for_extension(split.1))
+                        .map(|name| TaskSourceKind::Language { name: name.into() }),
+                    _ => None,
+                })
+                .or_else(|| {
+                    scenario.label.split_whitespace().find_map(|word| {
+                        language_names
+                            .iter()
+                            .find(|name| name.eq_ignore_ascii_case(word))
+                            .map(|name| TaskSourceKind::Language {
+                                name: name.to_owned().into(),
+                            })
+                    })
+                })
+        });
+
+        (language, scenario)
+    }
+
     pub fn task_contexts_loaded(
         &mut self,
         task_contexts: TaskContexts,
+        languages: Arc<LanguageRegistry>,
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) {
@@ -967,14 +1013,16 @@ impl DebugScenarioDelegate {
             self.last_used_candidate_index = Some(recent.len() - 1);
         }
 
+        let dap_registry = cx.global::<DapRegistry>();
+
         self.candidates = recent
             .into_iter()
-            .map(|scenario| (None, scenario))
-            .chain(
-                scenarios
-                    .into_iter()
-                    .map(|(kind, scenario)| (Some(kind), scenario)),
-            )
+            .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
+            .chain(scenarios.into_iter().map(|(kind, scenario)| {
+                let (language, scenario) =
+                    Self::get_scenario_kind(&languages, &dap_registry, scenario);
+                (language.or(Some(kind)), scenario)
+            }))
             .collect();
     }
 }

crates/language/src/language_registry.rs 🔗

@@ -68,6 +68,12 @@ impl From<LanguageName> for SharedString {
     }
 }
 
+impl From<SharedString> for LanguageName {
+    fn from(value: SharedString) -> Self {
+        LanguageName(value)
+    }
+}
+
 impl AsRef<str> for LanguageName {
     fn as_ref(&self) -> &str {
         self.0.as_ref()
@@ -627,6 +633,22 @@ impl LanguageRegistry {
         async move { rx.await? }
     }
 
+    pub fn language_name_for_extension(self: &Arc<Self>, extension: &str) -> Option<LanguageName> {
+        self.state.try_read().and_then(|state| {
+            state
+                .available_languages
+                .iter()
+                .find(|language| {
+                    language
+                        .matcher()
+                        .path_suffixes
+                        .iter()
+                        .any(|suffix| *suffix == extension)
+                })
+                .map(|language| language.name.clone())
+        })
+    }
+
     pub fn language_for_name_or_extension(
         self: &Arc<Self>,
         string: &str,