Detailed changes
@@ -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,
@@ -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()
+ })
+ }
}
@@ -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);
@@ -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 {
@@ -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()
+ }
+}
@@ -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)),
)
})
@@ -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),
})
}
@@ -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()],
})
@@ -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(),
});
@@ -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),
}
@@ -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",
+ "..."
+ ]
}
}
}