Merge branch 'main' into mic-denoising

David Kleingeld created

Change summary

crates/debugger_ui/src/debugger_panel.rs          |   4 
crates/debugger_ui/src/new_process_modal.rs       | 188 ++++++++++++----
crates/debugger_ui/src/tests/new_process_modal.rs |  78 ++++++
crates/edit_prediction_context/src/excerpt.rs     |   8 
crates/edit_prediction_context/src/reference.rs   |  71 ++++++
crates/git_ui/src/git_ui.rs                       |   2 
crates/language/src/language.rs                   |   5 
crates/languages/src/python.rs                    |  48 ++-
crates/project/src/task_inventory.rs              |   3 
crates/tasks_ui/src/modal.rs                      |   2 
docs/src/languages/python.md                      |   8 
11 files changed, 327 insertions(+), 90 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -134,6 +134,10 @@ impl DebugPanel {
             .map(|session| session.read(cx).running_state().clone())
     }
 
+    pub fn project(&self) -> &Entity<Project> {
+        &self.project
+    }
+
     pub fn load(
         workspace: WeakEntity<Workspace>,
         cx: &mut AsyncWindowContext,

crates/debugger_ui/src/new_process_modal.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::{Context as _, bail};
 use collections::{FxHashMap, HashMap, HashSet};
-use language::LanguageRegistry;
+use language::{LanguageName, LanguageRegistry};
 use std::{
     borrow::Cow,
     path::{Path, PathBuf},
@@ -22,7 +22,7 @@ use itertools::Itertools as _;
 use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
 use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
 use settings::Settings;
-use task::{DebugScenario, RevealTarget, ZedDebugConfig};
+use task::{DebugScenario, RevealTarget, VariableName, ZedDebugConfig};
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@@ -978,6 +978,7 @@ pub(super) struct DebugDelegate {
     task_store: Entity<TaskStore>,
     candidates: Vec<(
         Option<TaskSourceKind>,
+        Option<LanguageName>,
         DebugScenario,
         Option<DebugScenarioContext>,
     )>,
@@ -1005,28 +1006,89 @@ impl DebugDelegate {
         }
     }
 
-    fn get_scenario_kind(
+    fn get_task_subtitle(
+        &self,
+        task_kind: &Option<TaskSourceKind>,
+        context: &Option<DebugScenarioContext>,
+        cx: &mut App,
+    ) -> Option<String> {
+        match task_kind {
+            Some(TaskSourceKind::Worktree {
+                id: worktree_id,
+                directory_in_worktree,
+                ..
+            }) => self
+                .debug_panel
+                .update(cx, |debug_panel, cx| {
+                    let project = debug_panel.project().read(cx);
+                    let worktrees: Vec<_> = project.visible_worktrees(cx).collect();
+
+                    let mut path = if worktrees.len() > 1
+                        && let Some(worktree) = project.worktree_for_id(*worktree_id, cx)
+                    {
+                        let worktree_path = worktree.read(cx).abs_path();
+                        let full_path = worktree_path.join(directory_in_worktree);
+                        full_path
+                    } else {
+                        directory_in_worktree.clone()
+                    };
+
+                    match path
+                        .components()
+                        .next_back()
+                        .and_then(|component| component.as_os_str().to_str())
+                    {
+                        Some(".zed") => {
+                            path.push("debug.json");
+                        }
+                        Some(".vscode") => {
+                            path.push("launch.json");
+                        }
+                        _ => {}
+                    }
+                    Some(path.display().to_string())
+                })
+                .unwrap_or_else(|_| Some(directory_in_worktree.display().to_string())),
+            Some(TaskSourceKind::AbsPath { abs_path, .. }) => {
+                Some(abs_path.to_string_lossy().into_owned())
+            }
+            Some(TaskSourceKind::Lsp { language_name, .. }) => {
+                Some(format!("LSP: {language_name}"))
+            }
+            Some(TaskSourceKind::Language { .. }) => None,
+            _ => context.clone().and_then(|ctx| {
+                ctx.task_context
+                    .task_variables
+                    .get(&VariableName::RelativeFile)
+                    .map(|f| format!("in {f}"))
+                    .or_else(|| {
+                        ctx.task_context
+                            .task_variables
+                            .get(&VariableName::Dirname)
+                            .map(|d| format!("in {d}/"))
+                    })
+            }),
+        }
+    }
+
+    fn get_scenario_language(
         languages: &Arc<LanguageRegistry>,
         dap_registry: &DapRegistry,
         scenario: DebugScenario,
-    ) -> (Option<TaskSourceKind>, DebugScenario) {
+    ) -> (Option<LanguageName>, DebugScenario) {
         let language_names = languages.language_names();
-        let language = dap_registry
-            .adapter_language(&scenario.adapter)
-            .map(|language| TaskSourceKind::Language { name: language.0 });
+        let language_name = dap_registry.adapter_language(&scenario.adapter);
 
-        let language = language.or_else(|| {
+        let language_name = language_name.or_else(|| {
             scenario.label.split_whitespace().find_map(|word| {
                 language_names
                     .iter()
                     .find(|name| name.as_ref().eq_ignore_ascii_case(word))
-                    .map(|name| TaskSourceKind::Language {
-                        name: name.to_owned().into(),
-                    })
+                    .cloned()
             })
         });
 
-        (language, scenario)
+        (language_name, scenario)
     }
 
     pub fn tasks_loaded(
@@ -1080,9 +1142,9 @@ impl DebugDelegate {
                 this.delegate.candidates = recent
                     .into_iter()
                     .map(|(scenario, context)| {
-                        let (kind, scenario) =
-                            Self::get_scenario_kind(&languages, dap_registry, scenario);
-                        (kind, scenario, Some(context))
+                        let (language_name, scenario) =
+                            Self::get_scenario_language(&languages, dap_registry, scenario);
+                        (None, language_name, scenario, Some(context))
                     })
                     .chain(
                         scenarios
@@ -1097,9 +1159,9 @@ impl DebugDelegate {
                             })
                             .filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter))
                             .map(|(kind, scenario)| {
-                                let (language, scenario) =
-                                    Self::get_scenario_kind(&languages, dap_registry, scenario);
-                                (language.or(Some(kind)), scenario, None)
+                                let (language_name, scenario) =
+                                    Self::get_scenario_language(&languages, dap_registry, scenario);
+                                (Some(kind), language_name, scenario, None)
                             }),
                     )
                     .collect();
@@ -1145,7 +1207,7 @@ impl PickerDelegate for DebugDelegate {
             let candidates: Vec<_> = candidates
                 .into_iter()
                 .enumerate()
-                .map(|(index, (_, candidate, _))| {
+                .map(|(index, (_, _, candidate, _))| {
                     StringMatchCandidate::new(index, candidate.label.as_ref())
                 })
                 .collect();
@@ -1314,7 +1376,7 @@ impl PickerDelegate for DebugDelegate {
             .get(self.selected_index())
             .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
 
-        let Some((kind, debug_scenario, context)) = debug_scenario else {
+        let Some((kind, _, debug_scenario, context)) = debug_scenario else {
             return;
         };
 
@@ -1447,6 +1509,7 @@ impl PickerDelegate for DebugDelegate {
         cx: &mut Context<picker::Picker<Self>>,
     ) -> Option<Self::ListItem> {
         let hit = &self.matches.get(ix)?;
+        let (task_kind, language_name, _scenario, context) = &self.candidates[hit.candidate_id];
 
         let highlighted_location = HighlightedMatch {
             text: hit.string.clone(),
@@ -1454,33 +1517,40 @@ impl PickerDelegate for DebugDelegate {
             char_count: hit.string.chars().count(),
             color: Color::Default,
         };
-        let task_kind = &self.candidates[hit.candidate_id].0;
-
-        let icon = match task_kind {
-            Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)),
-            Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)),
-            Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)),
-            Some(TaskSourceKind::Lsp {
-                language_name: name,
-                ..
-            })
-            | Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx)
-                .get_icon_for_type(&name.to_lowercase(), cx)
-                .map(Icon::from_path),
-            None => Some(Icon::new(IconName::HistoryRerun)),
-        }
-        .map(|icon| icon.color(Color::Muted).size(IconSize::Small));
-        let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) {
-            Some(Indicator::icon(
-                Icon::new(IconName::BoltFilled)
-                    .color(Color::Muted)
-                    .size(IconSize::Small),
-            ))
-        } else {
-            None
+
+        let subtitle = self.get_task_subtitle(task_kind, context, cx);
+
+        let language_icon = language_name.as_ref().and_then(|lang| {
+            file_icons::FileIcons::get(cx)
+                .get_icon_for_type(&lang.0.to_lowercase(), cx)
+                .map(Icon::from_path)
+        });
+
+        let (icon, indicator) = match task_kind {
+            Some(TaskSourceKind::UserInput) => (Some(Icon::new(IconName::Terminal)), None),
+            Some(TaskSourceKind::AbsPath { .. }) => (Some(Icon::new(IconName::Settings)), None),
+            Some(TaskSourceKind::Worktree { .. }) => (Some(Icon::new(IconName::FileTree)), None),
+            Some(TaskSourceKind::Lsp { language_name, .. }) => (
+                file_icons::FileIcons::get(cx)
+                    .get_icon_for_type(&language_name.to_lowercase(), cx)
+                    .map(Icon::from_path),
+                Some(Indicator::icon(
+                    Icon::new(IconName::BoltFilled)
+                        .color(Color::Muted)
+                        .size(IconSize::Small),
+                )),
+            ),
+            Some(TaskSourceKind::Language { name }) => (
+                file_icons::FileIcons::get(cx)
+                    .get_icon_for_type(&name.to_lowercase(), cx)
+                    .map(Icon::from_path),
+                None,
+            ),
+            None => (Some(Icon::new(IconName::HistoryRerun)), None),
         };
-        let icon = icon.map(|icon| {
-            IconWithIndicator::new(icon, indicator)
+
+        let icon = language_icon.or(icon).map(|icon| {
+            IconWithIndicator::new(icon.color(Color::Muted).size(IconSize::Small), indicator)
                 .indicator_border_color(Some(cx.theme().colors().border_transparent))
         });
 
@@ -1490,7 +1560,18 @@ impl PickerDelegate for DebugDelegate {
                 .start_slot::<IconWithIndicator>(icon)
                 .spacing(ListItemSpacing::Sparse)
                 .toggle_state(selected)
-                .child(highlighted_location.render(window, cx)),
+                .child(
+                    v_flex()
+                        .items_start()
+                        .child(highlighted_location.render(window, cx))
+                        .when_some(subtitle, |this, subtitle_text| {
+                            this.child(
+                                Label::new(subtitle_text)
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            )
+                        }),
+                ),
         )
     }
 }
@@ -1539,4 +1620,17 @@ impl NewProcessModal {
             }
         })
     }
+
+    pub(crate) fn debug_picker_candidate_subtitles(&self, cx: &mut App) -> Vec<String> {
+        self.debug_picker.update(cx, |picker, cx| {
+            picker
+                .delegate
+                .candidates
+                .iter()
+                .filter_map(|(task_kind, _, _, context)| {
+                    picker.delegate.get_task_subtitle(task_kind, context, cx)
+                })
+                .collect()
+        })
+    }
 }

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

@@ -10,6 +10,7 @@ use text::Point;
 use util::path;
 
 use crate::NewProcessMode;
+use crate::new_process_modal::NewProcessModal;
 use crate::tests::{init_test, init_test_workspace};
 
 #[gpui::test]
@@ -178,13 +179,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
 
     workspace
         .update(cx, |workspace, window, cx| {
-            crate::new_process_modal::NewProcessModal::show(
-                workspace,
-                window,
-                NewProcessMode::Debug,
-                None,
-                cx,
-            );
+            NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
         })
         .unwrap();
 
@@ -192,7 +187,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
 
     let modal = workspace
         .update(cx, |workspace, _, cx| {
-            workspace.active_modal::<crate::new_process_modal::NewProcessModal>(cx)
+            workspace.active_modal::<NewProcessModal>(cx)
         })
         .unwrap()
         .expect("Modal should be active");
@@ -281,6 +276,73 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut
     pretty_assertions::assert_eq!(expected_content, debug_json_content);
 }
 
+#[gpui::test]
+async fn test_debug_modal_subtitles_with_multiple_worktrees(
+    executor: BackgroundExecutor,
+    cx: &mut TestAppContext,
+) {
+    init_test(cx);
+
+    let fs = FakeFs::new(executor.clone());
+
+    fs.insert_tree(
+        path!("/workspace1"),
+        json!({
+            ".zed": {
+                "debug.json": r#"[
+                    {
+                        "adapter": "fake-adapter",
+                        "label": "Debug App 1",
+                        "request": "launch",
+                        "program": "./app1",
+                        "cwd": "."
+                    },
+                    {
+                        "adapter": "fake-adapter",
+                        "label": "Debug Tests 1",
+                        "request": "launch",
+                        "program": "./test1",
+                        "cwd": "."
+                    }
+                ]"#
+            },
+            "main.rs": "fn main() {}"
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await;
+
+    let workspace = init_test_workspace(&project, cx).await;
+    let cx = &mut VisualTestContext::from_window(*workspace, cx);
+
+    workspace
+        .update(cx, |workspace, window, cx| {
+            NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
+        })
+        .unwrap();
+
+    cx.run_until_parked();
+
+    let modal = workspace
+        .update(cx, |workspace, _, cx| {
+            workspace.active_modal::<NewProcessModal>(cx)
+        })
+        .unwrap()
+        .expect("Modal should be active");
+
+    cx.executor().run_until_parked();
+
+    let subtitles = modal.update_in(cx, |modal, _, cx| {
+        modal.debug_picker_candidate_subtitles(cx)
+    });
+
+    assert_eq!(
+        subtitles.as_slice(),
+        [path!(".zed/debug.json"), path!(".zed/debug.json")]
+    );
+}
+
 #[gpui::test]
 async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) {
     init_test(cx);

crates/edit_prediction_context/src/excerpt.rs 🔗

@@ -114,13 +114,13 @@ impl EditPredictionExcerpt {
             options,
         };
 
-        if let Some(excerpt_ranges) = excerpt_selector.select_tree_sitter_nodes() {
-            if excerpt_ranges.size >= options.min_bytes {
-                return Some(excerpt_ranges);
+        if let Some(excerpt) = excerpt_selector.select_tree_sitter_nodes() {
+            if excerpt.size >= options.min_bytes {
+                return Some(excerpt);
             }
             log::debug!(
                 "tree-sitter excerpt was {} bytes, smaller than min of {}, falling back on line-based selection",
-                excerpt_ranges.size,
+                excerpt.size,
                 options.min_bytes
             );
         } else {

crates/edit_prediction_context/src/reference.rs 🔗

@@ -1,6 +1,7 @@
 use language::BufferSnapshot;
 use std::collections::HashMap;
 use std::ops::Range;
+use util::RangeExt;
 
 use crate::{
     declaration::Identifier,
@@ -78,8 +79,8 @@ pub fn identifiers_in_range(
             .highlights_config
             .as_ref();
 
-        for capture in mat.captures {
-            if let Some(config) = config {
+        if let Some(config) = config {
+            for capture in mat.captures {
                 if config.identifier_capture_indices.contains(&capture.index) {
                     let node_range = capture.node.byte_range();
 
@@ -88,9 +89,13 @@ pub fn identifiers_in_range(
                         continue;
                     }
 
+                    if !range.contains_inclusive(&node_range) {
+                        continue;
+                    }
+
                     let identifier_text =
-                        // TODO we changed this to saturating_sub for now, but we should fix the actually issue
-                        &range_text[node_range.start.saturating_sub(range.start)..node_range.end.saturating_sub(range.start)];
+                        &range_text[node_range.start - range.start..node_range.end - range.start];
+
                     references.push(Reference {
                         identifier: Identifier {
                             name: identifier_text.into(),
@@ -108,3 +113,61 @@ pub fn identifiers_in_range(
     }
     references
 }
+
+#[cfg(test)]
+mod test {
+    use gpui::{TestAppContext, prelude::*};
+    use indoc::indoc;
+    use language::{BufferSnapshot, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
+
+    use crate::reference::{ReferenceRegion, identifiers_in_range};
+
+    #[gpui::test]
+    fn test_identifier_node_truncated(cx: &mut TestAppContext) {
+        let code = indoc! { r#"
+            fn main() {
+                add(1, 2);
+            }
+
+            fn add(a: i32, b: i32) -> i32 {
+                a + b
+            }
+        "# };
+        let buffer = create_buffer(code, cx);
+
+        let range = 0..35;
+        let references = identifiers_in_range(
+            range.clone(),
+            &code[range],
+            ReferenceRegion::Breadcrumb,
+            &buffer,
+        );
+        assert_eq!(references.len(), 2);
+        assert_eq!(references[0].identifier.name.as_ref(), "main");
+        assert_eq!(references[1].identifier.name.as_ref(), "add");
+    }
+
+    fn create_buffer(text: &str, cx: &mut TestAppContext) -> BufferSnapshot {
+        let buffer =
+            cx.new(|cx| language::Buffer::local(text, cx).with_language(rust_lang().into(), cx));
+        buffer.read_with(cx, |buffer, _| buffer.snapshot())
+    }
+
+    fn rust_lang() -> Language {
+        Language::new(
+            LanguageConfig {
+                name: "Rust".into(),
+                matcher: LanguageMatcher {
+                    path_suffixes: vec!["rs".to_string()],
+                    ..Default::default()
+                },
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::LANGUAGE.into()),
+        )
+        .with_highlights_query(include_str!("../../languages/src/rust/highlights.scm"))
+        .unwrap()
+        .with_outline_query(include_str!("../../languages/src/rust/outline.scm"))
+        .unwrap()
+    }
+}

crates/git_ui/src/git_ui.rs 🔗

@@ -645,7 +645,6 @@ mod remote_button {
             this.child(
                 h_flex()
                     .ml_neg_0p5()
-                    .mr_1()
                     .when(behind_count > 0, |this| {
                         this.child(Icon::new(IconName::ArrowDown).size(IconSize::XSmall))
                             .child(count(behind_count))
@@ -660,7 +659,6 @@ mod remote_button {
             this.child(
                 h_flex()
                     .ml_neg_0p5()
-                    .mr_1()
                     .child(Icon::new(left_icon).size(IconSize::XSmall)),
             )
         })

crates/language/src/language.rs 🔗

@@ -55,7 +55,7 @@ use std::{
     str,
     sync::{
         Arc, LazyLock,
-        atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},
+        atomic::{AtomicUsize, Ordering::SeqCst},
     },
 };
 use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
@@ -168,7 +168,6 @@ pub struct CachedLspAdapter {
     pub disk_based_diagnostics_progress_token: Option<String>,
     language_ids: HashMap<LanguageName, String>,
     pub adapter: Arc<dyn LspAdapter>,
-    pub reinstall_attempt_count: AtomicU64,
     cached_binary: ServerBinaryCache,
 }
 
@@ -185,7 +184,6 @@ impl Debug for CachedLspAdapter {
                 &self.disk_based_diagnostics_progress_token,
             )
             .field("language_ids", &self.language_ids)
-            .field("reinstall_attempt_count", &self.reinstall_attempt_count)
             .finish_non_exhaustive()
     }
 }
@@ -204,7 +202,6 @@ impl CachedLspAdapter {
             language_ids,
             adapter,
             cached_binary: Default::default(),
-            reinstall_attempt_count: AtomicU64::new(0),
         })
     }
 

crates/languages/src/python.rs 🔗

@@ -1559,7 +1559,7 @@ impl LspInstaller for PyLspAdapter {
             util::command::new_smol_command(pip_path.as_path())
                 .arg("install")
                 .arg("python-lsp-server")
-                .arg("-U")
+                .arg("--upgrade")
                 .output()
                 .await?
                 .status
@@ -1570,7 +1570,7 @@ impl LspInstaller for PyLspAdapter {
             util::command::new_smol_command(pip_path.as_path())
                 .arg("install")
                 .arg("python-lsp-server[all]")
-                .arg("-U")
+                .arg("--upgrade")
                 .output()
                 .await?
                 .status
@@ -1581,7 +1581,7 @@ impl LspInstaller for PyLspAdapter {
             util::command::new_smol_command(pip_path)
                 .arg("install")
                 .arg("pylsp-mypy")
-                .arg("-U")
+                .arg("--upgrade")
                 .output()
                 .await?
                 .status
@@ -1589,6 +1589,10 @@ impl LspInstaller for PyLspAdapter {
             "pylsp-mypy installation failed"
         );
         let pylsp = venv.join(BINARY_DIR).join("pylsp");
+        ensure!(
+            delegate.which(pylsp.as_os_str()).await.is_some(),
+            "pylsp installation was incomplete"
+        );
         Ok(LanguageServerBinary {
             path: pylsp,
             env: None,
@@ -1603,6 +1607,7 @@ impl LspInstaller for PyLspAdapter {
     ) -> Option<LanguageServerBinary> {
         let venv = self.base_venv(delegate).await.ok()?;
         let pylsp = venv.join(BINARY_DIR).join("pylsp");
+        delegate.which(pylsp.as_os_str()).await?;
         Some(LanguageServerBinary {
             path: pylsp,
             env: None,
@@ -1641,9 +1646,11 @@ impl BasedPyrightLspAdapter {
                 .arg("venv")
                 .arg("basedpyright-venv")
                 .current_dir(work_dir)
-                .spawn()?
+                .spawn()
+                .context("spawning child")?
                 .output()
-                .await?;
+                .await
+                .context("getting child output")?;
         }
 
         Ok(path.into())
@@ -1885,11 +1892,14 @@ impl LspInstaller for BasedPyrightLspAdapter {
             let path = Path::new(toolchain?.path.as_ref())
                 .parent()?
                 .join(Self::BINARY_NAME);
-            path.exists().then(|| LanguageServerBinary {
-                path,
-                arguments: vec!["--stdio".into()],
-                env: None,
-            })
+            delegate
+                .which(path.as_os_str())
+                .await
+                .map(|_| LanguageServerBinary {
+                    path,
+                    arguments: vec!["--stdio".into()],
+                    env: None,
+                })
         }
     }
 
@@ -1905,16 +1915,21 @@ impl LspInstaller for BasedPyrightLspAdapter {
             util::command::new_smol_command(pip_path.as_path())
                 .arg("install")
                 .arg("basedpyright")
-                .arg("-U")
+                .arg("--upgrade")
                 .output()
-                .await?
+                .await
+                .context("getting pip install output")?
                 .status
                 .success(),
             "basedpyright installation failed"
         );
-        let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
+        let path = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
+        ensure!(
+            delegate.which(path.as_os_str()).await.is_some(),
+            "basedpyright installation was incomplete"
+        );
         Ok(LanguageServerBinary {
-            path: pylsp,
+            path,
             env: None,
             arguments: vec!["--stdio".into()],
         })
@@ -1926,9 +1941,10 @@ impl LspInstaller for BasedPyrightLspAdapter {
         delegate: &dyn LspAdapterDelegate,
     ) -> Option<LanguageServerBinary> {
         let venv = self.base_venv(delegate).await.ok()?;
-        let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
+        let path = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
+        delegate.which(path.as_os_str()).await?;
         Some(LanguageServerBinary {
-            path: pylsp,
+            path,
             env: None,
             arguments: vec!["--stdio".into()],
         })

crates/project/src/task_inventory.rs 🔗

@@ -258,7 +258,7 @@ impl Inventory {
     ) {
         self.last_scheduled_scenarios
             .retain(|(s, _)| s.label != scenario.label);
-        self.last_scheduled_scenarios.push_back((
+        self.last_scheduled_scenarios.push_front((
             scenario,
             DebugScenarioContext {
                 task_context,
@@ -388,6 +388,7 @@ impl Inventory {
             .into_iter()
             .flat_map(|worktree| self.worktree_templates_from_settings(worktree))
             .collect::<Vec<_>>();
+
         let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
             name: language.name().into(),
         });

crates/tasks_ui/src/modal.rs 🔗

@@ -493,7 +493,7 @@ impl PickerDelegate for TasksModalDelegate {
                 language_name: name,
                 ..
             }
-            | TaskSourceKind::Language { name } => file_icons::FileIcons::get(cx)
+            | TaskSourceKind::Language { name, .. } => file_icons::FileIcons::get(cx)
                 .get_icon_for_type(&name.to_lowercase(), cx)
                 .map(Icon::from_path),
         }

docs/src/languages/python.md 🔗

@@ -81,11 +81,13 @@ These are disabled by default, but can be enabled in your settings. For example:
 {
   "languages": {
     "Python": {
-      "language_servers": {
+      "language_servers": [
         // Disable basedpyright and enable Ty, and otherwise
         // use the default configuration.
-        "ty", "!basedpyright", ".."
-      }
+        "ty",
+        "!basedpyright",
+        "..."
+      ]
     }
   }
 }