From ac30ded80e210aefe061b1b19b540c96a229d4de Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 29 Feb 2024 01:18:13 +0200 Subject: [PATCH] Allow .zed/tasks.json local configs (#8536) ![image](https://github.com/zed-industries/zed/assets/2690773/e1511777-b4ca-469e-8636-1e513b615368) Follow-up of https://github.com/zed-industries/zed/issues/7108#issuecomment-1960746397 Makes more clear where each task came from, auto (re)load .zed/config.json changes, properly filtering out other worktree tasks. Release Notes: - Added local task configurations --- crates/languages/src/json.rs | 5 +- .../src/highlighted_match_with_paths.rs | 70 ++++ crates/picker/src/picker.rs | 2 + crates/project/src/project.rs | 62 ++- crates/project/src/project_tests.rs | 46 ++- crates/project/src/task_inventory.rs | 387 +++++++++++++++--- .../src/highlighted_workspace_location.rs | 130 ------ crates/recent_projects/src/recent_projects.rs | 118 ++++-- crates/task/src/static_source.rs | 19 +- crates/tasks_ui/src/modal.rs | 74 +++- crates/util/src/paths.rs | 1 + crates/zed/src/zed.rs | 82 +++- 12 files changed, 715 insertions(+), 281 deletions(-) create mode 100644 crates/picker/src/highlighted_match_with_paths.rs delete mode 100644 crates/recent_projects/src/highlighted_workspace_location.rs diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 2d2c4d1e2a..cee48ef4ec 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -72,7 +72,10 @@ impl JsonLspAdapter { "schema": KeymapFile::generate_json_schema(&action_names), }, { - "fileMatch": [schema_file_match(&paths::TASKS)], + "fileMatch": [ + schema_file_match(&paths::TASKS), + &*paths::LOCAL_TASKS_RELATIVE_PATH, + ], "schema": tasks_schema, } ] diff --git a/crates/picker/src/highlighted_match_with_paths.rs b/crates/picker/src/highlighted_match_with_paths.rs new file mode 100644 index 0000000000..02994c87a7 --- /dev/null +++ b/crates/picker/src/highlighted_match_with_paths.rs @@ -0,0 +1,70 @@ +use ui::{prelude::*, HighlightedLabel}; + +#[derive(Clone)] +pub struct HighlightedMatchWithPaths { + pub match_label: HighlightedText, + pub paths: Vec, +} + +#[derive(Debug, Clone, IntoElement)] +pub struct HighlightedText { + pub text: String, + pub highlight_positions: Vec, + pub char_count: usize, +} + +impl HighlightedText { + pub fn join(components: impl Iterator, separator: &str) -> Self { + let mut char_count = 0; + let separator_char_count = separator.chars().count(); + let mut text = String::new(); + let mut highlight_positions = Vec::new(); + for component in components { + if char_count != 0 { + text.push_str(separator); + char_count += separator_char_count; + } + + highlight_positions.extend( + component + .highlight_positions + .iter() + .map(|position| position + char_count), + ); + text.push_str(&component.text); + char_count += component.text.chars().count(); + } + + Self { + text, + highlight_positions, + char_count, + } + } +} + +impl RenderOnce for HighlightedText { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + HighlightedLabel::new(self.text, self.highlight_positions) + } +} + +impl HighlightedMatchWithPaths { + pub fn render_paths_children(&mut self, element: Div) -> Div { + element.children(self.paths.clone().into_iter().map(|path| { + HighlightedLabel::new(path.text, path.highlight_positions) + .size(LabelSize::Small) + .color(Color::Muted) + })) + } +} + +impl RenderOnce for HighlightedMatchWithPaths { + fn render(mut self, _: &mut WindowContext) -> impl IntoElement { + v_flex() + .child(self.match_label.clone()) + .when(!self.paths.is_empty(), |this| { + self.render_paths_children(this) + }) + } +} diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 55ee340035..fdf16be66f 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -8,6 +8,8 @@ use std::{sync::Arc, time::Duration}; use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing}; use workspace::ModalView; +pub mod highlighted_match_with_paths; + enum ElementContainer { List(ListState), UniformList(UniformListScrollHandle), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index af142d4ad6..9206b52c4a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -59,7 +59,7 @@ use rand::prelude::*; use rpc::{ErrorCode, ErrorExt as _}; use search::SearchQuery; use serde::Serialize; -use settings::{Settings, SettingsStore}; +use settings::{watch_config_file, Settings, SettingsStore}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use smol::channel::{Receiver, Sender}; @@ -82,11 +82,15 @@ use std::{ }, time::{Duration, Instant}, }; +use task::static_source::StaticSource; use terminals::Terminals; use text::{Anchor, BufferId}; use util::{ - debug_panic, defer, http::HttpClient, merge_json_value_into, - paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, + http::HttpClient, + merge_json_value_into, + paths::{LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH}, + post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; @@ -95,7 +99,7 @@ pub use language::Location; pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; pub use project_core::project_settings; pub use project_core::worktree::{self, *}; -pub use task_inventory::Inventory; +pub use task_inventory::{Inventory, TaskSourceKind}; const MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4; const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); @@ -6615,6 +6619,10 @@ impl Project { }) .detach(); + self.task_inventory().update(cx, |inventory, _| { + inventory.remove_worktree_sources(id_to_remove); + }); + self.worktrees.retain(|worktree| { if let Some(worktree) = worktree.upgrade() { let id = worktree.read(cx).id(); @@ -6972,32 +6980,66 @@ impl Project { changes: &UpdatedEntriesSet, cx: &mut ModelContext, ) { + if worktree.read(cx).as_local().is_none() { + return; + } let project_id = self.remote_id(); let worktree_id = worktree.entity_id(); - let worktree = worktree.read(cx).as_local().unwrap(); - let remote_worktree_id = worktree.id(); + let remote_worktree_id = worktree.read(cx).id(); let mut settings_contents = Vec::new(); for (path, _, change) in changes.iter() { - if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) { + let removed = change == &PathChange::Removed; + let abs_path = match worktree.read(cx).absolutize(path) { + Ok(abs_path) => abs_path, + Err(e) => { + log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}"); + continue; + } + }; + + if abs_path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) { let settings_dir = Arc::from( path.ancestors() .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count()) .unwrap(), ); let fs = self.fs.clone(); - let removed = *change == PathChange::Removed; - let abs_path = worktree.absolutize(path); settings_contents.push(async move { ( settings_dir, if removed { None } else { - Some(async move { fs.load(&abs_path?).await }.await) + Some(async move { fs.load(&abs_path).await }.await) }, ) }); + } else if abs_path.ends_with(&*LOCAL_TASKS_RELATIVE_PATH) { + self.task_inventory().update(cx, |task_inventory, cx| { + if removed { + task_inventory.remove_local_static_source(&abs_path); + } else { + let fs = self.fs.clone(); + let task_abs_path = abs_path.clone(); + task_inventory.add_source( + TaskSourceKind::Worktree { + id: remote_worktree_id, + abs_path, + }, + |cx| { + let tasks_file_rx = + watch_config_file(&cx.background_executor(), fs, task_abs_path); + StaticSource::new( + format!("local_tasks_for_workspace_{remote_worktree_id}"), + tasks_file_rx, + cx, + ) + }, + cx, + ); + } + }) } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index b222913325..69bff1a5e6 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -95,14 +95,24 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) "/the-root", json!({ ".zed": { - "settings.json": r#"{ "tab_size": 8 }"# + "settings.json": r#"{ "tab_size": 8 }"#, + "tasks.json": r#"[{ + "label": "cargo check", + "command": "cargo", + "args": ["check", "--all"] + },]"#, }, "a": { "a.rs": "fn a() {\n A\n}" }, "b": { ".zed": { - "settings.json": r#"{ "tab_size": 2 }"# + "settings.json": r#"{ "tab_size": 2 }"#, + "tasks.json": r#"[{ + "label": "cargo check", + "command": "cargo", + "args": ["check"] + },]"#, }, "b.rs": "fn b() {\n B\n}" } @@ -140,6 +150,38 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) assert_eq!(settings_a.tab_size.get(), 8); assert_eq!(settings_b.tab_size.get(), 2); + + let workree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + let all_tasks = project + .update(cx, |project, cx| { + project.task_inventory().update(cx, |inventory, cx| { + inventory.list_tasks(None, None, false, cx) + }) + }) + .into_iter() + .map(|(source_kind, task)| (source_kind, task.name().to_string())) + .collect::>(); + assert_eq!( + all_tasks, + vec![ + ( + TaskSourceKind::Worktree { + id: workree_id, + abs_path: PathBuf::from("/the-root/.zed/tasks.json") + }, + "cargo check".to_string() + ), + ( + TaskSourceKind::Worktree { + id: workree_id, + abs_path: PathBuf::from("/the-root/b/.zed/tasks.json") + }, + "cargo check".to_string() + ), + ] + ); }); } diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index a4ccd010fb..90d1850e8e 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -1,10 +1,15 @@ //! Project-wide storage of the tasks available, capable of updating itself from the sources set. -use std::{any::TypeId, path::Path, sync::Arc}; +use std::{ + any::TypeId, + path::{Path, PathBuf}, + sync::Arc, +}; use collections::{HashMap, VecDeque}; use gpui::{AppContext, Context, Model, ModelContext, Subscription}; use itertools::Itertools; +use project_core::worktree::WorktreeId; use task::{Task, TaskId, TaskSource}; use util::{post_inc, NumericPrefixWithSuffix}; @@ -18,6 +23,34 @@ struct SourceInInventory { source: Model>, _subscription: Subscription, type_id: TypeId, + kind: TaskSourceKind, +} + +/// Kind of a source the tasks are fetched from, used to display more source information in the UI. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TaskSourceKind { + /// bash-like commands spawned by users, not associated with any path + UserInput, + /// ~/.config/zed/task.json - like global files with task definitions, applicable to any path + AbsPath(PathBuf), + /// Worktree-specific task definitions, e.g. dynamic tasks from open worktree file, or tasks from the worktree's .zed/task.json + Worktree { id: WorktreeId, abs_path: PathBuf }, +} + +impl TaskSourceKind { + fn abs_path(&self) -> Option<&Path> { + match self { + Self::AbsPath(abs_path) | Self::Worktree { abs_path, .. } => Some(abs_path), + Self::UserInput => None, + } + } + + fn worktree(&self) -> Option { + match self { + Self::Worktree { id, .. } => Some(*id), + _ => None, + } + } } impl Inventory { @@ -28,21 +61,53 @@ impl Inventory { }) } - /// Registers a new tasks source, that would be fetched for available tasks. - pub fn add_source(&mut self, source: Model>, cx: &mut ModelContext) { - let _subscription = cx.observe(&source, |_, _, cx| { - cx.notify(); - }); + /// If the task with the same path was not added yet, + /// registers a new tasks source to fetch for available tasks later. + /// Unless a source is removed, ignores future additions for the same path. + pub fn add_source( + &mut self, + kind: TaskSourceKind, + create_source: impl FnOnce(&mut ModelContext) -> Model>, + cx: &mut ModelContext, + ) { + let abs_path = kind.abs_path(); + if abs_path.is_some() { + if let Some(a) = self.sources.iter().find(|s| s.kind.abs_path() == abs_path) { + log::debug!("Source for path {abs_path:?} already exists, not adding. Old kind: {OLD_KIND:?}, new kind: {kind:?}", OLD_KIND = a.kind); + return; + } + } + + let source = create_source(cx); let type_id = source.read(cx).type_id(); let source = SourceInInventory { + _subscription: cx.observe(&source, |_, _, cx| { + cx.notify(); + }), source, - _subscription, type_id, + kind, }; self.sources.push(source); cx.notify(); } + /// If present, removes the local static source entry that has the given path, + /// making corresponding task definitions unavailable in the fetch results. + /// + /// Now, entry for this path can be re-added again. + pub fn remove_local_static_source(&mut self, abs_path: &Path) { + self.sources.retain(|s| s.kind.abs_path() != Some(abs_path)); + } + + /// If present, removes the worktree source entry that has the given worktree id, + /// making corresponding task definitions unavailable in the fetch results. + /// + /// Now, entry for this path can be re-added again. + pub fn remove_worktree_sources(&mut self, worktree: WorktreeId) { + self.sources.retain(|s| s.kind.worktree() != Some(worktree)); + } + pub fn source(&self) -> Option>> { let target_type_id = std::any::TypeId::of::(); self.sources.iter().find_map( @@ -62,9 +127,10 @@ impl Inventory { pub fn list_tasks( &self, path: Option<&Path>, + worktree: Option, lru: bool, cx: &mut AppContext, - ) -> Vec> { + ) -> Vec<(TaskSourceKind, Arc)> { let mut lru_score = 0_u32; let tasks_by_usage = if lru { self.last_scheduled_tasks @@ -78,18 +144,23 @@ impl Inventory { HashMap::default() }; let not_used_score = post_inc(&mut lru_score); - self.sources .iter() + .filter(|source| { + let source_worktree = source.kind.worktree(); + worktree.is_none() || source_worktree.is_none() || source_worktree == worktree + }) .flat_map(|source| { source .source .update(cx, |source, cx| source.tasks_for_path(path, cx)) + .into_iter() + .map(|task| (&source.kind, task)) }) .map(|task| { let usages = if lru { tasks_by_usage - .get(&task.id()) + .get(&task.1.id()) .copied() .unwrap_or(not_used_score) } else { @@ -97,16 +168,34 @@ impl Inventory { }; (task, usages) }) - .sorted_unstable_by(|(task_a, usages_a), (task_b, usages_b)| { - usages_a.cmp(usages_b).then({ - NumericPrefixWithSuffix::from_numeric_prefixed_str(task_a.name()) - .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str( - task_b.name(), - )) - .then(task_a.name().cmp(task_b.name())) - }) - }) - .map(|(task, _)| task) + .sorted_unstable_by( + |((kind_a, task_a), usages_a), ((kind_b, task_b), usages_b)| { + usages_a + .cmp(usages_b) + .then( + kind_a + .worktree() + .is_none() + .cmp(&kind_b.worktree().is_none()), + ) + .then(kind_a.worktree().cmp(&kind_b.worktree())) + .then( + kind_a + .abs_path() + .is_none() + .cmp(&kind_b.abs_path().is_none()), + ) + .then(kind_a.abs_path().cmp(&kind_b.abs_path())) + .then({ + NumericPrefixWithSuffix::from_numeric_prefixed_str(task_a.name()) + .cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str( + task_b.name(), + )) + .then(task_a.name().cmp(task_b.name())) + }) + }, + ) + .map(|((kind, task), _)| (kind.clone(), task)) .collect() } @@ -114,9 +203,10 @@ impl Inventory { pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option> { self.last_scheduled_tasks.back().and_then(|id| { // TODO straighten the `Path` story to understand what has to be passed here: or it will break in the future. - self.list_tasks(None, false, cx) + self.list_tasks(None, None, false, cx) .into_iter() - .find(|task| task.id() == id) + .find(|(_, task)| task.id() == id) + .map(|(_, task)| task) }) } @@ -140,30 +230,37 @@ mod tests { #[gpui::test] fn test_task_list_sorting(cx: &mut TestAppContext) { let inventory = cx.update(Inventory::new); - let initial_tasks = list_task_names(&inventory, None, true, cx); + let initial_tasks = list_task_names(&inventory, None, None, true, cx); assert!( initial_tasks.is_empty(), "No tasks expected for empty inventory, but got {initial_tasks:?}" ); - let initial_tasks = list_task_names(&inventory, None, false, cx); + let initial_tasks = list_task_names(&inventory, None, None, false, cx); assert!( initial_tasks.is_empty(), "No tasks expected for empty inventory, but got {initial_tasks:?}" ); inventory.update(cx, |inventory, cx| { - inventory.add_source(TestSource::new(vec!["3_task".to_string()], cx), cx); + inventory.add_source( + TaskSourceKind::UserInput, + |cx| StaticTestSource::new(vec!["3_task".to_string()], cx), + cx, + ); }); inventory.update(cx, |inventory, cx| { inventory.add_source( - TestSource::new( - vec![ - "1_task".to_string(), - "2_task".to_string(), - "1_a_task".to_string(), - ], - cx, - ), + TaskSourceKind::UserInput, + |cx| { + StaticTestSource::new( + vec![ + "1_task".to_string(), + "2_task".to_string(), + "1_a_task".to_string(), + ], + cx, + ) + }, cx, ); }); @@ -175,24 +272,24 @@ mod tests { "3_task".to_string(), ]; assert_eq!( - list_task_names(&inventory, None, false, cx), + list_task_names(&inventory, None, None, false, cx), &expected_initial_state, "Task list without lru sorting, should be sorted alphanumerically" ); assert_eq!( - list_task_names(&inventory, None, true, cx), + list_task_names(&inventory, None, None, true, cx), &expected_initial_state, "Tasks with equal amount of usages should be sorted alphanumerically" ); register_task_used(&inventory, "2_task", cx); assert_eq!( - list_task_names(&inventory, None, false, cx), + list_task_names(&inventory, None, None, false, cx), &expected_initial_state, "Task list without lru sorting, should be sorted alphanumerically" ); assert_eq!( - list_task_names(&inventory, None, true, cx), + list_task_names(&inventory, None, None, true, cx), vec![ "2_task".to_string(), "1_a_task".to_string(), @@ -206,12 +303,12 @@ mod tests { register_task_used(&inventory, "1_task", cx); register_task_used(&inventory, "3_task", cx); assert_eq!( - list_task_names(&inventory, None, false, cx), + list_task_names(&inventory, None, None, false, cx), &expected_initial_state, "Task list without lru sorting, should be sorted alphanumerically" ); assert_eq!( - list_task_names(&inventory, None, true, cx), + list_task_names(&inventory, None, None, true, cx), vec![ "3_task".to_string(), "1_task".to_string(), @@ -222,7 +319,10 @@ mod tests { inventory.update(cx, |inventory, cx| { inventory.add_source( - TestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx), + TaskSourceKind::UserInput, + |cx| { + StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx) + }, cx, ); }); @@ -235,12 +335,12 @@ mod tests { "11_hello".to_string(), ]; assert_eq!( - list_task_names(&inventory, None, false, cx), + list_task_names(&inventory, None, None, false, cx), &expected_updated_state, "Task list without lru sorting, should be sorted alphanumerically" ); assert_eq!( - list_task_names(&inventory, None, true, cx), + list_task_names(&inventory, None, None, true, cx), vec![ "3_task".to_string(), "1_task".to_string(), @@ -253,12 +353,12 @@ mod tests { register_task_used(&inventory, "11_hello", cx); assert_eq!( - list_task_names(&inventory, None, false, cx), + list_task_names(&inventory, None, None, false, cx), &expected_updated_state, "Task list without lru sorting, should be sorted alphanumerically" ); assert_eq!( - list_task_names(&inventory, None, true, cx), + list_task_names(&inventory, None, None, true, cx), vec![ "11_hello".to_string(), "3_task".to_string(), @@ -270,6 +370,169 @@ mod tests { ); } + #[gpui::test] + fn test_inventory_static_task_filters(cx: &mut TestAppContext) { + let inventory_with_statics = cx.update(Inventory::new); + let common_name = "common_task_name"; + let path_1 = Path::new("path_1"); + let path_2 = Path::new("path_2"); + let worktree_1 = WorktreeId::from_usize(1); + let worktree_path_1 = Path::new("worktree_path_1"); + let worktree_2 = WorktreeId::from_usize(2); + let worktree_path_2 = Path::new("worktree_path_2"); + inventory_with_statics.update(cx, |inventory, cx| { + inventory.add_source( + TaskSourceKind::UserInput, + |cx| { + StaticTestSource::new( + vec!["user_input".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + inventory.add_source( + TaskSourceKind::AbsPath(path_1.to_path_buf()), + |cx| { + StaticTestSource::new( + vec!["static_source_1".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + inventory.add_source( + TaskSourceKind::AbsPath(path_2.to_path_buf()), + |cx| { + StaticTestSource::new( + vec!["static_source_2".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + inventory.add_source( + TaskSourceKind::Worktree { + id: worktree_1, + abs_path: worktree_path_1.to_path_buf(), + }, + |cx| { + StaticTestSource::new( + vec!["worktree_1".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + inventory.add_source( + TaskSourceKind::Worktree { + id: worktree_2, + abs_path: worktree_path_2.to_path_buf(), + }, + |cx| { + StaticTestSource::new( + vec!["worktree_2".to_string(), common_name.to_string()], + cx, + ) + }, + cx, + ); + }); + + let worktree_independent_tasks = vec![ + ( + TaskSourceKind::AbsPath(path_1.to_path_buf()), + common_name.to_string(), + ), + ( + TaskSourceKind::AbsPath(path_1.to_path_buf()), + "static_source_1".to_string(), + ), + ( + TaskSourceKind::AbsPath(path_2.to_path_buf()), + common_name.to_string(), + ), + ( + TaskSourceKind::AbsPath(path_2.to_path_buf()), + "static_source_2".to_string(), + ), + (TaskSourceKind::UserInput, common_name.to_string()), + (TaskSourceKind::UserInput, "user_input".to_string()), + ]; + let worktree_1_tasks = vec![ + ( + TaskSourceKind::Worktree { + id: worktree_1, + abs_path: worktree_path_1.to_path_buf(), + }, + common_name.to_string(), + ), + ( + TaskSourceKind::Worktree { + id: worktree_1, + abs_path: worktree_path_1.to_path_buf(), + }, + "worktree_1".to_string(), + ), + ]; + let worktree_2_tasks = vec![ + ( + TaskSourceKind::Worktree { + id: worktree_2, + abs_path: worktree_path_2.to_path_buf(), + }, + common_name.to_string(), + ), + ( + TaskSourceKind::Worktree { + id: worktree_2, + abs_path: worktree_path_2.to_path_buf(), + }, + "worktree_2".to_string(), + ), + ]; + + let all_tasks = worktree_1_tasks + .iter() + .chain(worktree_2_tasks.iter()) + // worktree-less tasks come later in the list + .chain(worktree_independent_tasks.iter()) + .cloned() + .collect::>(); + + for path in [ + None, + Some(path_1), + Some(path_2), + Some(worktree_path_1), + Some(worktree_path_2), + ] { + assert_eq!( + list_tasks(&inventory_with_statics, path, None, false, cx), + all_tasks, + "Path {path:?} choice should not adjust static runnables" + ); + assert_eq!( + list_tasks(&inventory_with_statics, path, Some(worktree_1), false, cx), + worktree_1_tasks + .iter() + .chain(worktree_independent_tasks.iter()) + .cloned() + .collect::>(), + "Path {path:?} choice should not adjust static runnables for worktree_1" + ); + assert_eq!( + list_tasks(&inventory_with_statics, path, Some(worktree_2), false, cx), + worktree_2_tasks + .iter() + .chain(worktree_independent_tasks.iter()) + .cloned() + .collect::>(), + "Path {path:?} choice should not adjust static runnables for worktree_2" + ); + } + } + #[derive(Debug, Clone, PartialEq, Eq)] struct TestTask { id: TaskId, @@ -294,11 +557,11 @@ mod tests { } } - struct TestSource { + struct StaticTestSource { tasks: Vec, } - impl TestSource { + impl StaticTestSource { fn new( task_names: impl IntoIterator, cx: &mut AppContext, @@ -318,10 +581,11 @@ mod tests { } } - impl TaskSource for TestSource { + impl TaskSource for StaticTestSource { fn tasks_for_path( &mut self, - _path: Option<&Path>, + // static task source does not depend on path input + _: Option<&Path>, _cx: &mut ModelContext>, ) -> Vec> { self.tasks @@ -339,24 +603,41 @@ mod tests { fn list_task_names( inventory: &Model, path: Option<&Path>, + worktree: Option, lru: bool, cx: &mut TestAppContext, ) -> Vec { inventory.update(cx, |inventory, cx| { inventory - .list_tasks(path, lru, cx) + .list_tasks(path, worktree, lru, cx) .into_iter() - .map(|task| task.name().to_string()) + .map(|(_, task)| task.name().to_string()) + .collect() + }) + } + + fn list_tasks( + inventory: &Model, + path: Option<&Path>, + worktree: Option, + lru: bool, + cx: &mut TestAppContext, + ) -> Vec<(TaskSourceKind, String)> { + inventory.update(cx, |inventory, cx| { + inventory + .list_tasks(path, worktree, lru, cx) + .into_iter() + .map(|(source_kind, task)| (source_kind, task.name().to_string())) .collect() }) } fn register_task_used(inventory: &Model, task_name: &str, cx: &mut TestAppContext) { inventory.update(cx, |inventory, cx| { - let task = inventory - .list_tasks(None, false, cx) + let (_, task) = inventory + .list_tasks(None, None, false, cx) .into_iter() - .find(|task| task.name() == task_name) + .find(|(_, task)| task.name() == task_name) .unwrap_or_else(|| panic!("Failed to find task with name {task_name}")); inventory.task_scheduled(task.id().clone()); }); diff --git a/crates/recent_projects/src/highlighted_workspace_location.rs b/crates/recent_projects/src/highlighted_workspace_location.rs deleted file mode 100644 index 436bafb062..0000000000 --- a/crates/recent_projects/src/highlighted_workspace_location.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std::path::Path; - -use fuzzy::StringMatch; -use ui::{prelude::*, HighlightedLabel}; -use util::paths::PathExt; -use workspace::WorkspaceLocation; - -#[derive(Clone, IntoElement)] -pub struct HighlightedText { - pub text: String, - pub highlight_positions: Vec, - char_count: usize, -} - -impl HighlightedText { - fn join(components: impl Iterator, separator: &str) -> Self { - let mut char_count = 0; - let separator_char_count = separator.chars().count(); - let mut text = String::new(); - let mut highlight_positions = Vec::new(); - for component in components { - if char_count != 0 { - text.push_str(separator); - char_count += separator_char_count; - } - - highlight_positions.extend( - component - .highlight_positions - .iter() - .map(|position| position + char_count), - ); - text.push_str(&component.text); - char_count += component.text.chars().count(); - } - - Self { - text, - highlight_positions, - char_count, - } - } -} - -impl RenderOnce for HighlightedText { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - HighlightedLabel::new(self.text, self.highlight_positions) - } -} - -#[derive(Clone)] -pub struct HighlightedWorkspaceLocation { - pub names: HighlightedText, - pub paths: Vec, -} - -impl HighlightedWorkspaceLocation { - pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self { - let mut path_start_offset = 0; - let (names, paths): (Vec<_>, Vec<_>) = location - .paths() - .iter() - .map(|path| { - let path = path.compact(); - let highlighted_text = Self::highlights_for_path( - path.as_ref(), - &string_match.positions, - path_start_offset, - ); - - path_start_offset += highlighted_text.1.char_count; - - highlighted_text - }) - .unzip(); - - Self { - names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "), - paths, - } - } - - // Compute the highlighted text for the name and path - fn highlights_for_path( - path: &Path, - match_positions: &Vec, - path_start_offset: usize, - ) -> (Option, HighlightedText) { - let path_string = path.to_string_lossy(); - let path_char_count = path_string.chars().count(); - // Get the subset of match highlight positions that line up with the given path. - // Also adjusts them to start at the path start - let path_positions = match_positions - .iter() - .copied() - .skip_while(|position| *position < path_start_offset) - .take_while(|position| *position < path_start_offset + path_char_count) - .map(|position| position - path_start_offset) - .collect::>(); - - // Again subset the highlight positions to just those that line up with the file_name - // again adjusted to the start of the file_name - let file_name_text_and_positions = path.file_name().map(|file_name| { - let text = file_name.to_string_lossy(); - let char_count = text.chars().count(); - let file_name_start = path_char_count - char_count; - let highlight_positions = path_positions - .iter() - .copied() - .skip_while(|position| *position < file_name_start) - .take_while(|position| *position < file_name_start + char_count) - .map(|position| position - file_name_start) - .collect::>(); - HighlightedText { - text: text.to_string(), - highlight_positions, - char_count, - } - }); - - ( - file_name_text_and_positions, - HighlightedText { - text: path_string.to_string(), - highlight_positions: path_positions, - char_count: path_char_count, - }, - ) - } -} diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index d43454183a..e3880d4525 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1,15 +1,15 @@ -mod highlighted_workspace_location; - use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task, View, ViewContext, WeakView, }; -use highlighted_workspace_location::HighlightedWorkspaceLocation; use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use std::sync::Arc; -use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing, Tooltip}; +use picker::{ + highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText}, + Picker, PickerDelegate, +}; +use std::{path::Path, sync::Arc}; +use ui::{prelude::*, tooltip_container, ListItem, ListItemSpacing, Tooltip}; use util::paths::PathExt; use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB}; @@ -245,32 +245,40 @@ impl PickerDelegate for RecentProjectsDelegate { selected: bool, cx: &mut ViewContext>, ) -> Option { - let Some(r#match) = self.matches.get(ix) else { + let Some(hit) = self.matches.get(ix) else { return None; }; - let (workspace_id, location) = &self.workspaces[r#match.candidate_id]; - let highlighted_location: HighlightedWorkspaceLocation = - HighlightedWorkspaceLocation::new(&r#match, location); - let tooltip_highlighted_location = highlighted_location.clone(); - + let (workspace_id, location) = &self.workspaces[hit.candidate_id]; let is_current_workspace = self.is_current_workspace(*workspace_id, cx); + + let mut path_start_offset = 0; + let (match_labels, paths): (Vec<_>, Vec<_>) = location + .paths() + .iter() + .map(|path| { + let path = path.compact(); + let highlighted_text = + highlights_for_path(path.as_ref(), &hit.positions, path_start_offset); + + path_start_offset += highlighted_text.1.char_count; + highlighted_text + }) + .unzip(); + + let highlighted_match = HighlightedMatchWithPaths { + match_label: HighlightedText::join( + match_labels.into_iter().filter_map(|name| name), + ", ", + ), + paths: if self.render_paths { paths } else { Vec::new() }, + }; Some( ListItem::new(ix) .inset(true) .spacing(ListItemSpacing::Sparse) .selected(selected) - .child( - v_flex() - .child(highlighted_location.names) - .when(self.render_paths, |this| { - this.children(highlighted_location.paths.into_iter().map(|path| { - HighlightedLabel::new(path.text, path.highlight_positions) - .size(LabelSize::Small) - .color(Color::Muted) - })) - }), - ) + .child(highlighted_match.clone().render(cx)) .when(!is_current_workspace, |el| { let delete_button = div() .child( @@ -293,7 +301,7 @@ impl PickerDelegate for RecentProjectsDelegate { } }) .tooltip(move |cx| { - let tooltip_highlighted_location = tooltip_highlighted_location.clone(); + let tooltip_highlighted_location = highlighted_match.clone(); cx.new_view(move |_| MatchTooltip { highlighted_location: tooltip_highlighted_location, }) @@ -303,6 +311,54 @@ impl PickerDelegate for RecentProjectsDelegate { } } +// Compute the highlighted text for the name and path +fn highlights_for_path( + path: &Path, + match_positions: &Vec, + path_start_offset: usize, +) -> (Option, HighlightedText) { + let path_string = path.to_string_lossy(); + let path_char_count = path_string.chars().count(); + // Get the subset of match highlight positions that line up with the given path. + // Also adjusts them to start at the path start + let path_positions = match_positions + .iter() + .copied() + .skip_while(|position| *position < path_start_offset) + .take_while(|position| *position < path_start_offset + path_char_count) + .map(|position| position - path_start_offset) + .collect::>(); + + // Again subset the highlight positions to just those that line up with the file_name + // again adjusted to the start of the file_name + let file_name_text_and_positions = path.file_name().map(|file_name| { + let text = file_name.to_string_lossy(); + let char_count = text.chars().count(); + let file_name_start = path_char_count - char_count; + let highlight_positions = path_positions + .iter() + .copied() + .skip_while(|position| *position < file_name_start) + .take_while(|position| *position < file_name_start + char_count) + .map(|position| position - file_name_start) + .collect::>(); + HighlightedText { + text: text.to_string(), + highlight_positions, + char_count, + } + }); + + ( + file_name_text_and_positions, + HighlightedText { + text: path_string.to_string(), + highlight_positions: path_positions, + char_count: path_char_count, + }, + ) +} + impl RecentProjectsDelegate { fn delete_recent_project(&self, ix: usize, cx: &mut ViewContext>) { if let Some(selected_match) = self.matches.get(ix) { @@ -340,23 +396,13 @@ impl RecentProjectsDelegate { } } struct MatchTooltip { - highlighted_location: HighlightedWorkspaceLocation, + highlighted_location: HighlightedMatchWithPaths, } impl Render for MatchTooltip { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { tooltip_container(cx, |div, _| { - div.children( - self.highlighted_location - .paths - .clone() - .into_iter() - .map(|path| { - HighlightedLabel::new(path.text, path.highlight_positions) - .size(LabelSize::Small) - .color(Color::Muted) - }), - ) + self.highlighted_location.render_paths_children(div) }) } } diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs index 7e2d59ad08..c178e65c96 100644 --- a/crates/task/src/static_source.rs +++ b/crates/task/src/static_source.rs @@ -1,6 +1,7 @@ //! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file. use std::{ + borrow::Cow, path::{Path, PathBuf}, sync::Arc, }; @@ -22,15 +23,6 @@ struct StaticTask { definition: Definition, } -impl StaticTask { - pub(super) fn new(id: usize, task_definition: Definition) -> Self { - Self { - id: TaskId(format!("static_{}_{}", task_definition.label, id)), - definition: task_definition, - } - } -} - impl Task for StaticTask { fn exec(&self, cwd: Option) -> Option { Some(SpawnInTerminal { @@ -150,14 +142,16 @@ impl Deserialize<'a> + PartialEq + 'static> TrackedFile { impl StaticSource { /// Initializes the static source, reacting on tasks config changes. pub fn new( + id_base: impl Into>, tasks_file_tracker: UnboundedReceiver, cx: &mut AppContext, ) -> Model> { let definitions = TrackedFile::new(DefinitionProvider::default(), tasks_file_tracker, cx); cx.new_model(|cx| { + let id_base = id_base.into(); let _subscription = cx.observe( &definitions, - |source: &mut Box<(dyn TaskSource + 'static)>, new_definitions, cx| { + move |source: &mut Box<(dyn TaskSource + 'static)>, new_definitions, cx| { if let Some(static_source) = source.as_any().downcast_mut::() { static_source.tasks = new_definitions .read(cx) @@ -166,7 +160,10 @@ impl StaticSource { .clone() .into_iter() .enumerate() - .map(|(id, definition)| StaticTask::new(id, definition)) + .map(|(i, definition)| StaticTask { + id: TaskId(format!("static_{id_base}_{i}_{}", definition.label)), + definition, + }) .collect(); cx.notify(); } diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index e02ae65f4f..dcc740a8dd 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ @@ -6,11 +6,14 @@ use gpui::{ Model, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; -use picker::{Picker, PickerDelegate}; -use project::Inventory; +use picker::{ + highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText}, + Picker, PickerDelegate, +}; +use project::{Inventory, ProjectPath, TaskSourceKind}; use task::{oneshot_source::OneshotSource, Task}; -use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable, WindowContext}; -use util::ResultExt; +use ui::{v_flex, ListItem, ListItemSpacing, RenderOnce, Selectable, WindowContext}; +use util::{paths::PathExt, ResultExt}; use workspace::{ModalView, Workspace}; use crate::schedule_task; @@ -20,7 +23,7 @@ actions!(task, [Spawn, Rerun]); /// A modal used to spawn new tasks. pub(crate) struct TasksModalDelegate { inventory: Model, - candidates: Vec>, + candidates: Vec<(TaskSourceKind, Arc)>, matches: Vec, selected_index: usize, workspace: WeakView, @@ -51,6 +54,21 @@ impl TasksModalDelegate { ) }) } + + fn active_item_path( + &mut self, + cx: &mut ViewContext<'_, Picker>, + ) -> Option<(PathBuf, ProjectPath)> { + let workspace = self.workspace.upgrade()?.read(cx); + let project = workspace.project().read(cx); + let active_item = workspace.active_item(cx)?; + active_item.project_path(cx).and_then(|project_path| { + project + .worktree_for_id(project_path.worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)) + .zip(Some(project_path)) + }) + } } pub(crate) struct TasksModal { @@ -130,16 +148,22 @@ impl PickerDelegate for TasksModalDelegate { cx.spawn(move |picker, mut cx| async move { let Some(candidates) = picker .update(&mut cx, |picker, cx| { - picker.delegate.candidates = picker - .delegate - .inventory - .update(cx, |inventory, cx| inventory.list_tasks(None, true, cx)); + let (path, worktree) = match picker.delegate.active_item_path(cx) { + Some((abs_path, project_path)) => { + (Some(abs_path), Some(project_path.worktree_id)) + } + None => (None, None), + }; + picker.delegate.candidates = + picker.delegate.inventory.update(cx, |inventory, cx| { + inventory.list_tasks(path.as_deref(), worktree, true, cx) + }); picker .delegate .candidates .iter() .enumerate() - .map(|(index, candidate)| StringMatchCandidate { + .map(|(index, (_, candidate))| StringMatchCandidate { id: index, char_bag: candidate.name().chars().collect(), string: candidate.name().into(), @@ -178,7 +202,6 @@ impl PickerDelegate for TasksModalDelegate { fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { let current_match_index = self.selected_index(); - let task = if secondary { if !self.prompt.trim().is_empty() { self.spawn_oneshot(cx) @@ -188,7 +211,7 @@ impl PickerDelegate for TasksModalDelegate { } else { self.matches.get(current_match_index).map(|current_match| { let ix = current_match.candidate_id; - self.candidates[ix].clone() + self.candidates[ix].1.clone() }) }; @@ -212,16 +235,35 @@ impl PickerDelegate for TasksModalDelegate { &self, ix: usize, selected: bool, - _cx: &mut ViewContext>, + cx: &mut ViewContext>, ) -> Option { let hit = &self.matches[ix]; - let highlights: Vec<_> = hit.positions.iter().copied().collect(); + let (source_kind, _) = &self.candidates[hit.candidate_id]; + let details = match source_kind { + TaskSourceKind::UserInput => "user input".to_string(), + TaskSourceKind::Worktree { abs_path, .. } | TaskSourceKind::AbsPath(abs_path) => { + abs_path.compact().to_string_lossy().to_string() + } + }; + + let highlighted_location = HighlightedMatchWithPaths { + match_label: HighlightedText { + text: hit.string.clone(), + highlight_positions: hit.positions.clone(), + char_count: hit.string.chars().count(), + }, + paths: vec![HighlightedText { + char_count: details.chars().count(), + highlight_positions: Vec::new(), + text: details, + }], + }; Some( ListItem::new(SharedString::from(format!("tasks-modal-{ix}"))) .inset(true) .spacing(ListItemSpacing::Sparse) .selected(selected) - .start_slot(HighlightedLabel::new(hit.string.clone(), highlights)), + .child(highlighted_location.render(cx)), ) } } diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 002c0998b9..9dbe64c418 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -44,6 +44,7 @@ lazy_static::lazy_static! { pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log"); pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old"); pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json"); + pub static ref LOCAL_TASKS_RELATIVE_PATH: &'static Path = Path::new(".zed/tasks.json"); pub static ref TEMP_DIR: PathBuf = HOME.join(".cache").join("zed"); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 604bb99968..b64452338d 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -14,24 +14,25 @@ use gpui::{ pub use only_instance::*; pub use open_listener::*; -use anyhow::{anyhow, Context as _}; +use anyhow::Context as _; use assets::Assets; use futures::{channel::mpsc, select_biased, StreamExt}; +use project::TaskSourceKind; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; use settings::{ - initial_local_settings_content, watch_config_file, KeymapFile, Settings, SettingsStore, - DEFAULT_KEYMAP_PATH, + initial_local_settings_content, initial_tasks_content, watch_config_file, KeymapFile, Settings, + SettingsStore, DEFAULT_KEYMAP_PATH, }; use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc}; use task::{oneshot_source::OneshotSource, static_source::StaticSource}; use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{ asset_str, - paths::{self, LOCAL_SETTINGS_RELATIVE_PATH}, + paths::{self, LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH}, ResultExt, }; use uuid::Uuid; @@ -59,6 +60,7 @@ actions!( OpenKeymap, OpenLicenses, OpenLocalSettings, + OpenLocalTasks, OpenLog, OpenTasks, OpenTelemetryLog, @@ -155,18 +157,26 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let project = workspace.project().clone(); if project.read(cx).is_local() { - let tasks_file_rx = watch_config_file( - &cx.background_executor(), - app_state.fs.clone(), - paths::TASKS.clone(), - ); - let static_source = StaticSource::new(tasks_file_rx, cx); - let oneshot_source = OneshotSource::new(cx); - project.update(cx, |project, cx| { + let fs = app_state.fs.clone(); project.task_inventory().update(cx, |inventory, cx| { - inventory.add_source(oneshot_source, cx); - inventory.add_source(static_source, cx); + inventory.add_source( + TaskSourceKind::UserInput, + |cx| OneshotSource::new(cx), + cx, + ); + inventory.add_source( + TaskSourceKind::AbsPath(paths::TASKS.clone()), + |cx| { + let tasks_file_rx = watch_config_file( + &cx.background_executor(), + fs, + paths::TASKS.clone(), + ); + StaticSource::new("global_tasks", tasks_file_rx, cx) + }, + cx, + ); }) }); } @@ -283,6 +293,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { }, ) .register_action(open_local_settings_file) + .register_action(open_local_tasks_file) .register_action( move |workspace: &mut Workspace, _: &OpenDefaultKeymap, @@ -602,6 +613,33 @@ fn open_local_settings_file( workspace: &mut Workspace, _: &OpenLocalSettings, cx: &mut ViewContext, +) { + open_local_file( + workspace, + &LOCAL_SETTINGS_RELATIVE_PATH, + initial_local_settings_content(), + cx, + ) +} + +fn open_local_tasks_file( + workspace: &mut Workspace, + _: &OpenLocalTasks, + cx: &mut ViewContext, +) { + open_local_file( + workspace, + &LOCAL_TASKS_RELATIVE_PATH, + initial_tasks_content(), + cx, + ) +} + +fn open_local_file( + workspace: &mut Workspace, + settings_relative_path: &'static Path, + initial_contents: Cow<'static, str>, + cx: &mut ViewContext, ) { let project = workspace.project().clone(); let worktree = project @@ -611,9 +649,7 @@ fn open_local_settings_file( if let Some(worktree) = worktree { let tree_id = worktree.read(cx).id(); cx.spawn(|workspace, mut cx| async move { - let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH; - - if let Some(dir_path) = file_path.parent() { + if let Some(dir_path) = settings_relative_path.parent() { if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { project .update(&mut cx, |project, cx| { @@ -624,10 +660,12 @@ fn open_local_settings_file( } } - if worktree.update(&mut cx, |tree, _| tree.entry_for_path(file_path).is_none())? { + if worktree.update(&mut cx, |tree, _| { + tree.entry_for_path(settings_relative_path).is_none() + })? { project .update(&mut cx, |project, cx| { - project.create_entry((tree_id, file_path), false, cx) + project.create_entry((tree_id, settings_relative_path), false, cx) })? .await .context("worktree was removed")?; @@ -635,11 +673,11 @@ fn open_local_settings_file( let editor = workspace .update(&mut cx, |workspace, cx| { - workspace.open_path((tree_id, file_path), None, true, cx) + workspace.open_path((tree_id, settings_relative_path), None, true, cx) })? .await? .downcast::() - .ok_or_else(|| anyhow!("unexpected item type"))?; + .context("unexpected item type: expected editor item")?; editor .downgrade() @@ -647,7 +685,7 @@ fn open_local_settings_file( if let Some(buffer) = editor.buffer().read(cx).as_singleton() { if buffer.read(cx).is_empty() { buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, initial_local_settings_content())], None, cx) + buffer.edit([(0..0, initial_contents)], None, cx) }); } }