mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
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
This commit is contained in:
parent
7f954cbbb8
commit
ac30ded80e
@ -72,7 +72,10 @@ impl JsonLspAdapter {
|
|||||||
"schema": KeymapFile::generate_json_schema(&action_names),
|
"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,
|
"schema": tasks_schema,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
70
crates/picker/src/highlighted_match_with_paths.rs
Normal file
70
crates/picker/src/highlighted_match_with_paths.rs
Normal file
@ -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 ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing};
|
||||||
use workspace::ModalView;
|
use workspace::ModalView;
|
||||||
|
|
||||||
|
pub mod highlighted_match_with_paths;
|
||||||
|
|
||||||
enum ElementContainer {
|
enum ElementContainer {
|
||||||
List(ListState),
|
List(ListState),
|
||||||
UniformList(UniformListScrollHandle),
|
UniformList(UniformListScrollHandle),
|
||||||
|
@ -59,7 +59,7 @@ use rand::prelude::*;
|
|||||||
use rpc::{ErrorCode, ErrorExt as _};
|
use rpc::{ErrorCode, ErrorExt as _};
|
||||||
use search::SearchQuery;
|
use search::SearchQuery;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{watch_config_file, Settings, SettingsStore};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
use smol::channel::{Receiver, Sender};
|
use smol::channel::{Receiver, Sender};
|
||||||
@ -82,11 +82,15 @@ use std::{
|
|||||||
},
|
},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
use task::static_source::StaticSource;
|
||||||
use terminals::Terminals;
|
use terminals::Terminals;
|
||||||
use text::{Anchor, BufferId};
|
use text::{Anchor, BufferId};
|
||||||
use util::{
|
use util::{
|
||||||
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
debug_panic, defer,
|
||||||
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
http::HttpClient,
|
||||||
|
merge_json_value_into,
|
||||||
|
paths::{LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH},
|
||||||
|
post_inc, ResultExt, TryFutureExt as _,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use fs::*;
|
pub use fs::*;
|
||||||
@ -95,7 +99,7 @@ pub use language::Location;
|
|||||||
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX;
|
||||||
pub use project_core::project_settings;
|
pub use project_core::project_settings;
|
||||||
pub use project_core::worktree::{self, *};
|
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 MAX_SERVER_REINSTALL_ATTEMPT_COUNT: u64 = 4;
|
||||||
const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
@ -6615,6 +6619,10 @@ impl Project {
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
self.task_inventory().update(cx, |inventory, _| {
|
||||||
|
inventory.remove_worktree_sources(id_to_remove);
|
||||||
|
});
|
||||||
|
|
||||||
self.worktrees.retain(|worktree| {
|
self.worktrees.retain(|worktree| {
|
||||||
if let Some(worktree) = worktree.upgrade() {
|
if let Some(worktree) = worktree.upgrade() {
|
||||||
let id = worktree.read(cx).id();
|
let id = worktree.read(cx).id();
|
||||||
@ -6972,32 +6980,66 @@ impl Project {
|
|||||||
changes: &UpdatedEntriesSet,
|
changes: &UpdatedEntriesSet,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
|
if worktree.read(cx).as_local().is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let project_id = self.remote_id();
|
let project_id = self.remote_id();
|
||||||
let worktree_id = worktree.entity_id();
|
let worktree_id = worktree.entity_id();
|
||||||
let worktree = worktree.read(cx).as_local().unwrap();
|
let remote_worktree_id = worktree.read(cx).id();
|
||||||
let remote_worktree_id = worktree.id();
|
|
||||||
|
|
||||||
let mut settings_contents = Vec::new();
|
let mut settings_contents = Vec::new();
|
||||||
for (path, _, change) in changes.iter() {
|
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(
|
let settings_dir = Arc::from(
|
||||||
path.ancestors()
|
path.ancestors()
|
||||||
.nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count())
|
.nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count())
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let removed = *change == PathChange::Removed;
|
|
||||||
let abs_path = worktree.absolutize(path);
|
|
||||||
settings_contents.push(async move {
|
settings_contents.push(async move {
|
||||||
(
|
(
|
||||||
settings_dir,
|
settings_dir,
|
||||||
if removed {
|
if removed {
|
||||||
None
|
None
|
||||||
} else {
|
} 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",
|
"/the-root",
|
||||||
json!({
|
json!({
|
||||||
".zed": {
|
".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": {
|
||||||
"a.rs": "fn a() {\n A\n}"
|
"a.rs": "fn a() {\n A\n}"
|
||||||
},
|
},
|
||||||
"b": {
|
"b": {
|
||||||
".zed": {
|
".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}"
|
"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_a.tab_size.get(), 8);
|
||||||
assert_eq!(settings_b.tab_size.get(), 2);
|
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.
|
//! 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 collections::{HashMap, VecDeque};
|
||||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use project_core::worktree::WorktreeId;
|
||||||
use task::{Task, TaskId, TaskSource};
|
use task::{Task, TaskId, TaskSource};
|
||||||
use util::{post_inc, NumericPrefixWithSuffix};
|
use util::{post_inc, NumericPrefixWithSuffix};
|
||||||
|
|
||||||
@ -18,6 +23,34 @@ struct SourceInInventory {
|
|||||||
source: Model<Box<dyn TaskSource>>,
|
source: Model<Box<dyn TaskSource>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
type_id: TypeId,
|
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 {
|
impl Inventory {
|
||||||
@ -28,21 +61,53 @@ impl Inventory {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a new tasks source, that would be fetched for available tasks.
|
/// If the task with the same path was not added yet,
|
||||||
pub fn add_source(&mut self, source: Model<Box<dyn TaskSource>>, cx: &mut ModelContext<Self>) {
|
/// registers a new tasks source to fetch for available tasks later.
|
||||||
let _subscription = cx.observe(&source, |_, _, cx| {
|
/// Unless a source is removed, ignores future additions for the same path.
|
||||||
cx.notify();
|
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 type_id = source.read(cx).type_id();
|
||||||
let source = SourceInInventory {
|
let source = SourceInInventory {
|
||||||
|
_subscription: cx.observe(&source, |_, _, cx| {
|
||||||
|
cx.notify();
|
||||||
|
}),
|
||||||
source,
|
source,
|
||||||
_subscription,
|
|
||||||
type_id,
|
type_id,
|
||||||
|
kind,
|
||||||
};
|
};
|
||||||
self.sources.push(source);
|
self.sources.push(source);
|
||||||
cx.notify();
|
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>>> {
|
pub fn source<T: TaskSource>(&self) -> Option<Model<Box<dyn TaskSource>>> {
|
||||||
let target_type_id = std::any::TypeId::of::<T>();
|
let target_type_id = std::any::TypeId::of::<T>();
|
||||||
self.sources.iter().find_map(
|
self.sources.iter().find_map(
|
||||||
@ -62,9 +127,10 @@ impl Inventory {
|
|||||||
pub fn list_tasks(
|
pub fn list_tasks(
|
||||||
&self,
|
&self,
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
|
worktree: Option<WorktreeId>,
|
||||||
lru: bool,
|
lru: bool,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Vec<Arc<dyn Task>> {
|
) -> Vec<(TaskSourceKind, Arc<dyn Task>)> {
|
||||||
let mut lru_score = 0_u32;
|
let mut lru_score = 0_u32;
|
||||||
let tasks_by_usage = if lru {
|
let tasks_by_usage = if lru {
|
||||||
self.last_scheduled_tasks
|
self.last_scheduled_tasks
|
||||||
@ -78,18 +144,23 @@ impl Inventory {
|
|||||||
HashMap::default()
|
HashMap::default()
|
||||||
};
|
};
|
||||||
let not_used_score = post_inc(&mut lru_score);
|
let not_used_score = post_inc(&mut lru_score);
|
||||||
|
|
||||||
self.sources
|
self.sources
|
||||||
.iter()
|
.iter()
|
||||||
|
.filter(|source| {
|
||||||
|
let source_worktree = source.kind.worktree();
|
||||||
|
worktree.is_none() || source_worktree.is_none() || source_worktree == worktree
|
||||||
|
})
|
||||||
.flat_map(|source| {
|
.flat_map(|source| {
|
||||||
source
|
source
|
||||||
.source
|
.source
|
||||||
.update(cx, |source, cx| source.tasks_for_path(path, cx))
|
.update(cx, |source, cx| source.tasks_for_path(path, cx))
|
||||||
|
.into_iter()
|
||||||
|
.map(|task| (&source.kind, task))
|
||||||
})
|
})
|
||||||
.map(|task| {
|
.map(|task| {
|
||||||
let usages = if lru {
|
let usages = if lru {
|
||||||
tasks_by_usage
|
tasks_by_usage
|
||||||
.get(&task.id())
|
.get(&task.1.id())
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(not_used_score)
|
.unwrap_or(not_used_score)
|
||||||
} else {
|
} else {
|
||||||
@ -97,16 +168,34 @@ impl Inventory {
|
|||||||
};
|
};
|
||||||
(task, usages)
|
(task, usages)
|
||||||
})
|
})
|
||||||
.sorted_unstable_by(|(task_a, usages_a), (task_b, usages_b)| {
|
.sorted_unstable_by(
|
||||||
usages_a.cmp(usages_b).then({
|
|((kind_a, task_a), usages_a), ((kind_b, task_b), usages_b)| {
|
||||||
NumericPrefixWithSuffix::from_numeric_prefixed_str(task_a.name())
|
usages_a
|
||||||
.cmp(&NumericPrefixWithSuffix::from_numeric_prefixed_str(
|
.cmp(usages_b)
|
||||||
task_b.name(),
|
.then(
|
||||||
))
|
kind_a
|
||||||
.then(task_a.name().cmp(task_b.name()))
|
.worktree()
|
||||||
})
|
.is_none()
|
||||||
})
|
.cmp(&kind_b.worktree().is_none()),
|
||||||
.map(|(task, _)| task)
|
)
|
||||||
|
.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()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,9 +203,10 @@ impl Inventory {
|
|||||||
pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option<Arc<dyn Task>> {
|
pub fn last_scheduled_task(&self, cx: &mut AppContext) -> Option<Arc<dyn Task>> {
|
||||||
self.last_scheduled_tasks.back().and_then(|id| {
|
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.
|
// 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()
|
.into_iter()
|
||||||
.find(|task| task.id() == id)
|
.find(|(_, task)| task.id() == id)
|
||||||
|
.map(|(_, task)| task)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,30 +230,37 @@ mod tests {
|
|||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_task_list_sorting(cx: &mut TestAppContext) {
|
fn test_task_list_sorting(cx: &mut TestAppContext) {
|
||||||
let inventory = cx.update(Inventory::new);
|
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!(
|
assert!(
|
||||||
initial_tasks.is_empty(),
|
initial_tasks.is_empty(),
|
||||||
"No tasks expected for empty inventory, but got {initial_tasks:?}"
|
"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!(
|
assert!(
|
||||||
initial_tasks.is_empty(),
|
initial_tasks.is_empty(),
|
||||||
"No tasks expected for empty inventory, but got {initial_tasks:?}"
|
"No tasks expected for empty inventory, but got {initial_tasks:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
inventory.update(cx, |inventory, cx| {
|
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.update(cx, |inventory, cx| {
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
TestSource::new(
|
TaskSourceKind::UserInput,
|
||||||
vec![
|
|cx| {
|
||||||
"1_task".to_string(),
|
StaticTestSource::new(
|
||||||
"2_task".to_string(),
|
vec![
|
||||||
"1_a_task".to_string(),
|
"1_task".to_string(),
|
||||||
],
|
"2_task".to_string(),
|
||||||
cx,
|
"1_a_task".to_string(),
|
||||||
),
|
],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -175,24 +272,24 @@ mod tests {
|
|||||||
"3_task".to_string(),
|
"3_task".to_string(),
|
||||||
];
|
];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, false, cx),
|
list_task_names(&inventory, None, None, false, cx),
|
||||||
&expected_initial_state,
|
&expected_initial_state,
|
||||||
"Task list without lru sorting, should be sorted alphanumerically"
|
"Task list without lru sorting, should be sorted alphanumerically"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, true, cx),
|
list_task_names(&inventory, None, None, true, cx),
|
||||||
&expected_initial_state,
|
&expected_initial_state,
|
||||||
"Tasks with equal amount of usages should be sorted alphanumerically"
|
"Tasks with equal amount of usages should be sorted alphanumerically"
|
||||||
);
|
);
|
||||||
|
|
||||||
register_task_used(&inventory, "2_task", cx);
|
register_task_used(&inventory, "2_task", cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, false, cx),
|
list_task_names(&inventory, None, None, false, cx),
|
||||||
&expected_initial_state,
|
&expected_initial_state,
|
||||||
"Task list without lru sorting, should be sorted alphanumerically"
|
"Task list without lru sorting, should be sorted alphanumerically"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, true, cx),
|
list_task_names(&inventory, None, None, true, cx),
|
||||||
vec![
|
vec![
|
||||||
"2_task".to_string(),
|
"2_task".to_string(),
|
||||||
"1_a_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, "1_task", cx);
|
||||||
register_task_used(&inventory, "3_task", cx);
|
register_task_used(&inventory, "3_task", cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, false, cx),
|
list_task_names(&inventory, None, None, false, cx),
|
||||||
&expected_initial_state,
|
&expected_initial_state,
|
||||||
"Task list without lru sorting, should be sorted alphanumerically"
|
"Task list without lru sorting, should be sorted alphanumerically"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, true, cx),
|
list_task_names(&inventory, None, None, true, cx),
|
||||||
vec![
|
vec![
|
||||||
"3_task".to_string(),
|
"3_task".to_string(),
|
||||||
"1_task".to_string(),
|
"1_task".to_string(),
|
||||||
@ -222,7 +319,10 @@ mod tests {
|
|||||||
|
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, cx| {
|
||||||
inventory.add_source(
|
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,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -235,12 +335,12 @@ mod tests {
|
|||||||
"11_hello".to_string(),
|
"11_hello".to_string(),
|
||||||
];
|
];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, false, cx),
|
list_task_names(&inventory, None, None, false, cx),
|
||||||
&expected_updated_state,
|
&expected_updated_state,
|
||||||
"Task list without lru sorting, should be sorted alphanumerically"
|
"Task list without lru sorting, should be sorted alphanumerically"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, true, cx),
|
list_task_names(&inventory, None, None, true, cx),
|
||||||
vec![
|
vec![
|
||||||
"3_task".to_string(),
|
"3_task".to_string(),
|
||||||
"1_task".to_string(),
|
"1_task".to_string(),
|
||||||
@ -253,12 +353,12 @@ mod tests {
|
|||||||
|
|
||||||
register_task_used(&inventory, "11_hello", cx);
|
register_task_used(&inventory, "11_hello", cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, false, cx),
|
list_task_names(&inventory, None, None, false, cx),
|
||||||
&expected_updated_state,
|
&expected_updated_state,
|
||||||
"Task list without lru sorting, should be sorted alphanumerically"
|
"Task list without lru sorting, should be sorted alphanumerically"
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
list_task_names(&inventory, None, true, cx),
|
list_task_names(&inventory, None, None, true, cx),
|
||||||
vec![
|
vec![
|
||||||
"11_hello".to_string(),
|
"11_hello".to_string(),
|
||||||
"3_task".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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
struct TestTask {
|
struct TestTask {
|
||||||
id: TaskId,
|
id: TaskId,
|
||||||
@ -294,11 +557,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestSource {
|
struct StaticTestSource {
|
||||||
tasks: Vec<TestTask>,
|
tasks: Vec<TestTask>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestSource {
|
impl StaticTestSource {
|
||||||
fn new(
|
fn new(
|
||||||
task_names: impl IntoIterator<Item = String>,
|
task_names: impl IntoIterator<Item = String>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
@ -318,10 +581,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaskSource for TestSource {
|
impl TaskSource for StaticTestSource {
|
||||||
fn tasks_for_path(
|
fn tasks_for_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
_path: Option<&Path>,
|
// static task source does not depend on path input
|
||||||
|
_: Option<&Path>,
|
||||||
_cx: &mut ModelContext<Box<dyn TaskSource>>,
|
_cx: &mut ModelContext<Box<dyn TaskSource>>,
|
||||||
) -> Vec<Arc<dyn Task>> {
|
) -> Vec<Arc<dyn Task>> {
|
||||||
self.tasks
|
self.tasks
|
||||||
@ -339,24 +603,41 @@ mod tests {
|
|||||||
fn list_task_names(
|
fn list_task_names(
|
||||||
inventory: &Model<Inventory>,
|
inventory: &Model<Inventory>,
|
||||||
path: Option<&Path>,
|
path: Option<&Path>,
|
||||||
|
worktree: Option<WorktreeId>,
|
||||||
lru: bool,
|
lru: bool,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, cx| {
|
||||||
inventory
|
inventory
|
||||||
.list_tasks(path, lru, cx)
|
.list_tasks(path, worktree, lru, cx)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|task| task.name().to_string())
|
.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(|(source_kind, task)| (source_kind, task.name().to_string()))
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_task_used(inventory: &Model<Inventory>, task_name: &str, cx: &mut TestAppContext) {
|
fn register_task_used(inventory: &Model<Inventory>, task_name: &str, cx: &mut TestAppContext) {
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, cx| {
|
||||||
let task = inventory
|
let (_, task) = inventory
|
||||||
.list_tasks(None, false, cx)
|
.list_tasks(None, None, false, cx)
|
||||||
.into_iter()
|
.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}"));
|
.unwrap_or_else(|| panic!("Failed to find task with name {task_name}"));
|
||||||
inventory.task_scheduled(task.id().clone());
|
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 fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result,
|
AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result,
|
||||||
Subscription, Task, View, ViewContext, WeakView,
|
Subscription, Task, View, ViewContext, WeakView,
|
||||||
};
|
};
|
||||||
use highlighted_workspace_location::HighlightedWorkspaceLocation;
|
|
||||||
use ordered_float::OrderedFloat;
|
use ordered_float::OrderedFloat;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{
|
||||||
use std::sync::Arc;
|
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
||||||
use ui::{prelude::*, tooltip_container, HighlightedLabel, ListItem, ListItemSpacing, Tooltip};
|
Picker, PickerDelegate,
|
||||||
|
};
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
use ui::{prelude::*, tooltip_container, ListItem, ListItemSpacing, Tooltip};
|
||||||
use util::paths::PathExt;
|
use util::paths::PathExt;
|
||||||
use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
|
use workspace::{ModalView, Workspace, WorkspaceId, WorkspaceLocation, WORKSPACE_DB};
|
||||||
|
|
||||||
@ -245,32 +245,40 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
selected: bool,
|
selected: bool,
|
||||||
cx: &mut ViewContext<Picker<Self>>,
|
cx: &mut ViewContext<Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let Some(r#match) = self.matches.get(ix) else {
|
let Some(hit) = self.matches.get(ix) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (workspace_id, location) = &self.workspaces[r#match.candidate_id];
|
let (workspace_id, location) = &self.workspaces[hit.candidate_id];
|
||||||
let highlighted_location: HighlightedWorkspaceLocation =
|
|
||||||
HighlightedWorkspaceLocation::new(&r#match, location);
|
|
||||||
let tooltip_highlighted_location = highlighted_location.clone();
|
|
||||||
|
|
||||||
let is_current_workspace = self.is_current_workspace(*workspace_id, cx);
|
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(
|
Some(
|
||||||
ListItem::new(ix)
|
ListItem::new(ix)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.selected(selected)
|
.selected(selected)
|
||||||
.child(
|
.child(highlighted_match.clone().render(cx))
|
||||||
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)
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.when(!is_current_workspace, |el| {
|
.when(!is_current_workspace, |el| {
|
||||||
let delete_button = div()
|
let delete_button = div()
|
||||||
.child(
|
.child(
|
||||||
@ -293,7 +301,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.tooltip(move |cx| {
|
.tooltip(move |cx| {
|
||||||
let tooltip_highlighted_location = tooltip_highlighted_location.clone();
|
let tooltip_highlighted_location = highlighted_match.clone();
|
||||||
cx.new_view(move |_| MatchTooltip {
|
cx.new_view(move |_| MatchTooltip {
|
||||||
highlighted_location: tooltip_highlighted_location,
|
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 {
|
impl RecentProjectsDelegate {
|
||||||
fn delete_recent_project(&self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
fn delete_recent_project(&self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
if let Some(selected_match) = self.matches.get(ix) {
|
if let Some(selected_match) = self.matches.get(ix) {
|
||||||
@ -340,23 +396,13 @@ impl RecentProjectsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct MatchTooltip {
|
struct MatchTooltip {
|
||||||
highlighted_location: HighlightedWorkspaceLocation,
|
highlighted_location: HighlightedMatchWithPaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for MatchTooltip {
|
impl Render for MatchTooltip {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
tooltip_container(cx, |div, _| {
|
tooltip_container(cx, |div, _| {
|
||||||
div.children(
|
self.highlighted_location.render_paths_children(div)
|
||||||
self.highlighted_location
|
|
||||||
.paths
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|path| {
|
|
||||||
HighlightedLabel::new(path.text, path.highlight_positions)
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
//! 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::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
@ -22,15 +23,6 @@ struct StaticTask {
|
|||||||
definition: Definition,
|
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 {
|
impl Task for StaticTask {
|
||||||
fn exec(&self, cwd: Option<PathBuf>) -> Option<SpawnInTerminal> {
|
fn exec(&self, cwd: Option<PathBuf>) -> Option<SpawnInTerminal> {
|
||||||
Some(SpawnInTerminal {
|
Some(SpawnInTerminal {
|
||||||
@ -150,14 +142,16 @@ impl<T: for<'a> Deserialize<'a> + PartialEq + 'static> TrackedFile<T> {
|
|||||||
impl StaticSource {
|
impl StaticSource {
|
||||||
/// Initializes the static source, reacting on tasks config changes.
|
/// Initializes the static source, reacting on tasks config changes.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
id_base: impl Into<Cow<'static, str>>,
|
||||||
tasks_file_tracker: UnboundedReceiver<String>,
|
tasks_file_tracker: UnboundedReceiver<String>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Model<Box<dyn TaskSource>> {
|
) -> Model<Box<dyn TaskSource>> {
|
||||||
let definitions = TrackedFile::new(DefinitionProvider::default(), tasks_file_tracker, cx);
|
let definitions = TrackedFile::new(DefinitionProvider::default(), tasks_file_tracker, cx);
|
||||||
cx.new_model(|cx| {
|
cx.new_model(|cx| {
|
||||||
|
let id_base = id_base.into();
|
||||||
let _subscription = cx.observe(
|
let _subscription = cx.observe(
|
||||||
&definitions,
|
&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>() {
|
if let Some(static_source) = source.as_any().downcast_mut::<Self>() {
|
||||||
static_source.tasks = new_definitions
|
static_source.tasks = new_definitions
|
||||||
.read(cx)
|
.read(cx)
|
||||||
@ -166,7 +160,10 @@ impl StaticSource {
|
|||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(id, definition)| StaticTask::new(id, definition))
|
.map(|(i, definition)| StaticTask {
|
||||||
|
id: TaskId(format!("static_{id_base}_{i}_{}", definition.label)),
|
||||||
|
definition,
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::sync::Arc;
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -6,11 +6,14 @@ use gpui::{
|
|||||||
Model, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext,
|
Model, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext,
|
||||||
VisualContext, WeakView,
|
VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{
|
||||||
use project::Inventory;
|
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
|
||||||
|
Picker, PickerDelegate,
|
||||||
|
};
|
||||||
|
use project::{Inventory, ProjectPath, TaskSourceKind};
|
||||||
use task::{oneshot_source::OneshotSource, Task};
|
use task::{oneshot_source::OneshotSource, Task};
|
||||||
use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable, WindowContext};
|
use ui::{v_flex, ListItem, ListItemSpacing, RenderOnce, Selectable, WindowContext};
|
||||||
use util::ResultExt;
|
use util::{paths::PathExt, ResultExt};
|
||||||
use workspace::{ModalView, Workspace};
|
use workspace::{ModalView, Workspace};
|
||||||
|
|
||||||
use crate::schedule_task;
|
use crate::schedule_task;
|
||||||
@ -20,7 +23,7 @@ actions!(task, [Spawn, Rerun]);
|
|||||||
/// A modal used to spawn new tasks.
|
/// A modal used to spawn new tasks.
|
||||||
pub(crate) struct TasksModalDelegate {
|
pub(crate) struct TasksModalDelegate {
|
||||||
inventory: Model<Inventory>,
|
inventory: Model<Inventory>,
|
||||||
candidates: Vec<Arc<dyn Task>>,
|
candidates: Vec<(TaskSourceKind, Arc<dyn Task>)>,
|
||||||
matches: Vec<StringMatch>,
|
matches: Vec<StringMatch>,
|
||||||
selected_index: usize,
|
selected_index: usize,
|
||||||
workspace: WeakView<Workspace>,
|
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 {
|
pub(crate) struct TasksModal {
|
||||||
@ -130,16 +148,22 @@ impl PickerDelegate for TasksModalDelegate {
|
|||||||
cx.spawn(move |picker, mut cx| async move {
|
cx.spawn(move |picker, mut cx| async move {
|
||||||
let Some(candidates) = picker
|
let Some(candidates) = picker
|
||||||
.update(&mut cx, |picker, cx| {
|
.update(&mut cx, |picker, cx| {
|
||||||
picker.delegate.candidates = picker
|
let (path, worktree) = match picker.delegate.active_item_path(cx) {
|
||||||
.delegate
|
Some((abs_path, project_path)) => {
|
||||||
.inventory
|
(Some(abs_path), Some(project_path.worktree_id))
|
||||||
.update(cx, |inventory, cx| inventory.list_tasks(None, true, cx));
|
}
|
||||||
|
None => (None, None),
|
||||||
|
};
|
||||||
|
picker.delegate.candidates =
|
||||||
|
picker.delegate.inventory.update(cx, |inventory, cx| {
|
||||||
|
inventory.list_tasks(path.as_deref(), worktree, true, cx)
|
||||||
|
});
|
||||||
picker
|
picker
|
||||||
.delegate
|
.delegate
|
||||||
.candidates
|
.candidates
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, candidate)| StringMatchCandidate {
|
.map(|(index, (_, candidate))| StringMatchCandidate {
|
||||||
id: index,
|
id: index,
|
||||||
char_bag: candidate.name().chars().collect(),
|
char_bag: candidate.name().chars().collect(),
|
||||||
string: candidate.name().into(),
|
string: candidate.name().into(),
|
||||||
@ -178,7 +202,6 @@ impl PickerDelegate for TasksModalDelegate {
|
|||||||
|
|
||||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
|
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
|
||||||
let current_match_index = self.selected_index();
|
let current_match_index = self.selected_index();
|
||||||
|
|
||||||
let task = if secondary {
|
let task = if secondary {
|
||||||
if !self.prompt.trim().is_empty() {
|
if !self.prompt.trim().is_empty() {
|
||||||
self.spawn_oneshot(cx)
|
self.spawn_oneshot(cx)
|
||||||
@ -188,7 +211,7 @@ impl PickerDelegate for TasksModalDelegate {
|
|||||||
} else {
|
} else {
|
||||||
self.matches.get(current_match_index).map(|current_match| {
|
self.matches.get(current_match_index).map(|current_match| {
|
||||||
let ix = current_match.candidate_id;
|
let ix = current_match.candidate_id;
|
||||||
self.candidates[ix].clone()
|
self.candidates[ix].1.clone()
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -212,16 +235,35 @@ impl PickerDelegate for TasksModalDelegate {
|
|||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
_cx: &mut ViewContext<picker::Picker<Self>>,
|
cx: &mut ViewContext<picker::Picker<Self>>,
|
||||||
) -> Option<Self::ListItem> {
|
) -> Option<Self::ListItem> {
|
||||||
let hit = &self.matches[ix];
|
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(
|
Some(
|
||||||
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
|
ListItem::new(SharedString::from(format!("tasks-modal-{ix}")))
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.spacing(ListItemSpacing::Sparse)
|
.spacing(ListItemSpacing::Sparse)
|
||||||
.selected(selected)
|
.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 LOG: PathBuf = LOGS_DIR.join("Zed.log");
|
||||||
pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
|
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_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");
|
pub static ref TEMP_DIR: PathBuf = HOME.join(".cache").join("zed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,24 +14,25 @@ use gpui::{
|
|||||||
pub use only_instance::*;
|
pub use only_instance::*;
|
||||||
pub use open_listener::*;
|
pub use open_listener::*;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::Context as _;
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use futures::{channel::mpsc, select_biased, StreamExt};
|
use futures::{channel::mpsc, select_biased, StreamExt};
|
||||||
|
use project::TaskSourceKind;
|
||||||
use project_panel::ProjectPanel;
|
use project_panel::ProjectPanel;
|
||||||
use quick_action_bar::QuickActionBar;
|
use quick_action_bar::QuickActionBar;
|
||||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use search::project_search::ProjectSearchBar;
|
use search::project_search::ProjectSearchBar;
|
||||||
use settings::{
|
use settings::{
|
||||||
initial_local_settings_content, watch_config_file, KeymapFile, Settings, SettingsStore,
|
initial_local_settings_content, initial_tasks_content, watch_config_file, KeymapFile, Settings,
|
||||||
DEFAULT_KEYMAP_PATH,
|
SettingsStore, DEFAULT_KEYMAP_PATH,
|
||||||
};
|
};
|
||||||
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
||||||
use task::{oneshot_source::OneshotSource, static_source::StaticSource};
|
use task::{oneshot_source::OneshotSource, static_source::StaticSource};
|
||||||
use terminal_view::terminal_panel::{self, TerminalPanel};
|
use terminal_view::terminal_panel::{self, TerminalPanel};
|
||||||
use util::{
|
use util::{
|
||||||
asset_str,
|
asset_str,
|
||||||
paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
|
paths::{self, LOCAL_SETTINGS_RELATIVE_PATH, LOCAL_TASKS_RELATIVE_PATH},
|
||||||
ResultExt,
|
ResultExt,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@ -59,6 +60,7 @@ actions!(
|
|||||||
OpenKeymap,
|
OpenKeymap,
|
||||||
OpenLicenses,
|
OpenLicenses,
|
||||||
OpenLocalSettings,
|
OpenLocalSettings,
|
||||||
|
OpenLocalTasks,
|
||||||
OpenLog,
|
OpenLog,
|
||||||
OpenTasks,
|
OpenTasks,
|
||||||
OpenTelemetryLog,
|
OpenTelemetryLog,
|
||||||
@ -155,18 +157,26 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||||||
|
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
if project.read(cx).is_local() {
|
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| {
|
project.update(cx, |project, cx| {
|
||||||
|
let fs = app_state.fs.clone();
|
||||||
project.task_inventory().update(cx, |inventory, cx| {
|
project.task_inventory().update(cx, |inventory, cx| {
|
||||||
inventory.add_source(oneshot_source, cx);
|
inventory.add_source(
|
||||||
inventory.add_source(static_source, cx);
|
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_settings_file)
|
||||||
|
.register_action(open_local_tasks_file)
|
||||||
.register_action(
|
.register_action(
|
||||||
move |workspace: &mut Workspace,
|
move |workspace: &mut Workspace,
|
||||||
_: &OpenDefaultKeymap,
|
_: &OpenDefaultKeymap,
|
||||||
@ -602,6 +613,33 @@ fn open_local_settings_file(
|
|||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_: &OpenLocalSettings,
|
_: &OpenLocalSettings,
|
||||||
cx: &mut ViewContext<Workspace>,
|
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 project = workspace.project().clone();
|
||||||
let worktree = project
|
let worktree = project
|
||||||
@ -611,9 +649,7 @@ fn open_local_settings_file(
|
|||||||
if let Some(worktree) = worktree {
|
if let Some(worktree) = worktree {
|
||||||
let tree_id = worktree.read(cx).id();
|
let tree_id = worktree.read(cx).id();
|
||||||
cx.spawn(|workspace, mut cx| async move {
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH;
|
if let Some(dir_path) = settings_relative_path.parent() {
|
||||||
|
|
||||||
if let Some(dir_path) = file_path.parent() {
|
|
||||||
if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
|
if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
|
||||||
project
|
project
|
||||||
.update(&mut cx, |project, cx| {
|
.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
|
project
|
||||||
.update(&mut cx, |project, cx| {
|
.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
|
.await
|
||||||
.context("worktree was removed")?;
|
.context("worktree was removed")?;
|
||||||
@ -635,11 +673,11 @@ fn open_local_settings_file(
|
|||||||
|
|
||||||
let editor = workspace
|
let editor = workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.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?
|
.await?
|
||||||
.downcast::<Editor>()
|
.downcast::<Editor>()
|
||||||
.ok_or_else(|| anyhow!("unexpected item type"))?;
|
.context("unexpected item type: expected editor item")?;
|
||||||
|
|
||||||
editor
|
editor
|
||||||
.downgrade()
|
.downgrade()
|
||||||
@ -647,7 +685,7 @@ fn open_local_settings_file(
|
|||||||
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
|
||||||
if buffer.read(cx).is_empty() {
|
if buffer.read(cx).is_empty() {
|
||||||
buffer.update(cx, |buffer, cx| {
|
buffer.update(cx, |buffer, cx| {
|
||||||
buffer.edit([(0..0, initial_local_settings_content())], None, cx)
|
buffer.edit([(0..0, initial_contents)], None, cx)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user