Detailed changes
@@ -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,
}
]
@@ -0,0 +1,70 @@
+use ui::{prelude::*, HighlightedLabel};
+
+#[derive(Clone)]
+pub struct HighlightedMatchWithPaths {
+ pub match_label: HighlightedText,
+ pub paths: Vec<HighlightedText>,
+}
+
+#[derive(Debug, Clone, IntoElement)]
+pub struct HighlightedText {
+ pub text: String,
+ pub highlight_positions: Vec<usize>,
+ pub char_count: usize,
+}
+
+impl HighlightedText {
+ pub fn join(components: impl Iterator<Item = Self>, 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)
+ })
+ }
+}
@@ -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),
@@ -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<Self>,
) {
+ 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,
+ );
+ }
+ })
}
}
@@ -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::<Vec<_>>();
+ 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()
+ ),
+ ]
+ );
});
}
@@ -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<Box<dyn TaskSource>>,
_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<WorktreeId> {
+ 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<Box<dyn TaskSource>>, cx: &mut ModelContext<Self>) {
- 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<Self>) -> Model<Box<dyn TaskSource>>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ 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<T: TaskSource>(&self) -> Option<Model<Box<dyn TaskSource>>> {
let target_type_id = std::any::TypeId::of::<T>();
self.sources.iter().find_map(
@@ -62,9 +127,10 @@ impl Inventory {
pub fn list_tasks(
&self,
path: Option<&Path>,
+ worktree: Option<WorktreeId>,
lru: bool,
cx: &mut AppContext,
- ) -> Vec<Arc<dyn Task>> {
+ ) -> Vec<(TaskSourceKind, Arc<dyn Task>)> {
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<Arc<dyn Task>> {
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::<Vec<_>>();
+
+ 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::<Vec<_>>(),
+ "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::<Vec<_>>(),
+ "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<TestTask>,
}
- impl TestSource {
+ impl StaticTestSource {
fn new(
task_names: impl IntoIterator<Item = String>,
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<Box<dyn TaskSource>>,
) -> Vec<Arc<dyn Task>> {
self.tasks
@@ -339,24 +603,41 @@ mod tests {
fn list_task_names(
inventory: &Model<Inventory>,
path: Option<&Path>,
+ worktree: Option<WorktreeId>,
lru: bool,
cx: &mut TestAppContext,
) -> Vec<String> {
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())
+ .collect()
+ })
+ }
+
+ fn list_tasks(
+ inventory: &Model<Inventory>,
+ path: Option<&Path>,
+ worktree: Option<WorktreeId>,
+ lru: bool,
+ cx: &mut TestAppContext,
+ ) -> Vec<(TaskSourceKind, String)> {
+ inventory.update(cx, |inventory, cx| {
+ inventory
+ .list_tasks(path, worktree, lru, cx)
.into_iter()
- .map(|task| task.name().to_string())
+ .map(|(source_kind, task)| (source_kind, task.name().to_string()))
.collect()
})
}
fn register_task_used(inventory: &Model<Inventory>, 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());
});
@@ -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<usize>,
- char_count: usize,
-}
-
-impl HighlightedText {
- fn join(components: impl Iterator<Item = Self>, 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<HighlightedText>,
-}
-
-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<usize>,
- path_start_offset: usize,
- ) -> (Option<HighlightedText>, 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::<Vec<_>>();
-
- // 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::<Vec<_>>();
- 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,
- },
- )
- }
-}
@@ -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<Picker<Self>>,
) -> Option<Self::ListItem> {
- 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<usize>,
+ path_start_offset: usize,
+) -> (Option<HighlightedText>, 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::<Vec<_>>();
+
+ // 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::<Vec<_>>();
+ 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<Picker<Self>>) {
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<Self>) -> 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)
})
}
}
@@ -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<PathBuf>) -> Option<SpawnInTerminal> {
Some(SpawnInTerminal {
@@ -150,14 +142,16 @@ impl<T: for<'a> Deserialize<'a> + PartialEq + 'static> TrackedFile<T> {
impl StaticSource {
/// Initializes the static source, reacting on tasks config changes.
pub fn new(
+ id_base: impl Into<Cow<'static, str>>,
tasks_file_tracker: UnboundedReceiver<String>,
cx: &mut AppContext,
) -> Model<Box<dyn TaskSource>> {
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::<Self>() {
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();
}
@@ -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<Inventory>,
- candidates: Vec<Arc<dyn Task>>,
+ candidates: Vec<(TaskSourceKind, Arc<dyn Task>)>,
matches: Vec<StringMatch>,
selected_index: usize,
workspace: WeakView<Workspace>,
@@ -51,6 +54,21 @@ impl TasksModalDelegate {
)
})
}
+
+ fn active_item_path(
+ &mut self,
+ cx: &mut ViewContext<'_, Picker<Self>>,
+ ) -> 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<picker::Picker<Self>>) {
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<picker::Picker<Self>>,
+ cx: &mut ViewContext<picker::Picker<Self>>,
) -> Option<Self::ListItem> {
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)),
)
}
}
@@ -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");
}
@@ -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<AppState>, 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<AppState>, 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<Workspace>,
+) {
+ 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<Workspace>,
+) {
+ 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<Workspace>,
) {
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::<Editor>()
- .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)
});
}
}