Add support for detecting tests in source files, and implement it for Rust (#11195)
Continuing work from #10873 Release Notes: - N/A --------- Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
14c7782ce6
commit
5a71d8c7f1
29 changed files with 1148 additions and 606 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -3394,6 +3394,7 @@ dependencies = [
|
||||||
"smol",
|
"smol",
|
||||||
"snippet",
|
"snippet",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
|
"task",
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"time",
|
"time",
|
||||||
|
@ -9875,6 +9876,7 @@ dependencies = [
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
"hex",
|
"hex",
|
||||||
|
"parking_lot",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json_lenient",
|
"serde_json_lenient",
|
||||||
|
@ -9887,7 +9889,6 @@ dependencies = [
|
||||||
name = "tasks_ui"
|
name = "tasks_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"editor",
|
"editor",
|
||||||
"file_icons",
|
"file_icons",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
|
|
@ -667,7 +667,7 @@ async fn test_collaborating_with_code_actions(
|
||||||
editor_b.update(cx_b, |editor, cx| {
|
editor_b.update(cx_b, |editor, cx| {
|
||||||
editor.toggle_code_actions(
|
editor.toggle_code_actions(
|
||||||
&ToggleCodeActions {
|
&ToggleCodeActions {
|
||||||
deployed_from_indicator: false,
|
deployed_from_indicator: None,
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
|
@ -60,6 +60,7 @@ smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
snippet.workspace = true
|
snippet.workspace = true
|
||||||
sum_tree.workspace = true
|
sum_tree.workspace = true
|
||||||
|
task.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
time_format.workspace = true
|
time_format.workspace = true
|
||||||
|
|
|
@ -53,7 +53,13 @@ pub struct SelectToEndOfLine {
|
||||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||||
pub struct ToggleCodeActions {
|
pub struct ToggleCodeActions {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub deployed_from_indicator: bool,
|
pub deployed_from_indicator: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||||
|
pub struct ToggleTestRunner {
|
||||||
|
#[serde(default)]
|
||||||
|
pub deployed_from_row: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||||
|
|
|
@ -34,6 +34,7 @@ mod persistence;
|
||||||
mod rust_analyzer_ext;
|
mod rust_analyzer_ext;
|
||||||
pub mod scroll;
|
pub mod scroll;
|
||||||
mod selections_collection;
|
mod selections_collection;
|
||||||
|
pub mod tasks;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod editor_tests;
|
mod editor_tests;
|
||||||
|
@ -78,6 +79,7 @@ use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||||
pub use inline_completion_provider::*;
|
pub use inline_completion_provider::*;
|
||||||
pub use items::MAX_TAB_TITLE_LEN;
|
pub use items::MAX_TAB_TITLE_LEN;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use language::Runnable;
|
||||||
use language::{
|
use language::{
|
||||||
char_kind,
|
char_kind,
|
||||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||||
|
@ -85,6 +87,7 @@ use language::{
|
||||||
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||||
Point, Selection, SelectionGoal, TransactionId,
|
Point, Selection, SelectionGoal, TransactionId,
|
||||||
};
|
};
|
||||||
|
use task::{ResolvedTask, TaskTemplate};
|
||||||
|
|
||||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight};
|
||||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||||
|
@ -99,7 +102,8 @@ use ordered_float::OrderedFloat;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
use project::project_settings::{GitGutterSetting, ProjectSettings};
|
||||||
use project::{
|
use project::{
|
||||||
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath, ProjectTransaction,
|
CodeAction, Completion, FormatTrigger, Item, Location, Project, ProjectPath,
|
||||||
|
ProjectTransaction, TaskSourceKind, WorktreeId,
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rpc::{proto::*, ErrorExt};
|
use rpc::{proto::*, ErrorExt};
|
||||||
|
@ -395,6 +399,19 @@ impl Default for ScrollbarMarkerState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct RunnableTasks {
|
||||||
|
templates: SmallVec<[(TaskSourceKind, TaskTemplate); 1]>,
|
||||||
|
// We need the column at which the task context evaluation should take place.
|
||||||
|
column: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct ResolvedTasks {
|
||||||
|
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||||
|
position: text::Point,
|
||||||
|
}
|
||||||
|
|
||||||
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
|
/// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`]
|
||||||
///
|
///
|
||||||
/// See the [module level documentation](self) for more information.
|
/// See the [module level documentation](self) for more information.
|
||||||
|
@ -487,6 +504,7 @@ pub struct Editor {
|
||||||
>,
|
>,
|
||||||
last_bounds: Option<Bounds<Pixels>>,
|
last_bounds: Option<Bounds<Pixels>>,
|
||||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||||
|
tasks: HashMap<u32, RunnableTasks>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -1180,12 +1198,106 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
struct CodeActionContents {
|
||||||
|
tasks: Option<Arc<ResolvedTasks>>,
|
||||||
|
actions: Option<Arc<[CodeAction]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeActionContents {
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
match (&self.tasks, &self.actions) {
|
||||||
|
(Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
|
||||||
|
(Some(tasks), None) => tasks.templates.len(),
|
||||||
|
(None, Some(actions)) => actions.len(),
|
||||||
|
(None, None) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
match (&self.tasks, &self.actions) {
|
||||||
|
(Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
|
||||||
|
(Some(tasks), None) => tasks.templates.is_empty(),
|
||||||
|
(None, Some(actions)) => actions.is_empty(),
|
||||||
|
(None, None) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
|
||||||
|
self.tasks
|
||||||
|
.iter()
|
||||||
|
.flat_map(|tasks| {
|
||||||
|
tasks
|
||||||
|
.templates
|
||||||
|
.iter()
|
||||||
|
.map(|(kind, task)| CodeActionsItem::Task(kind.clone(), task.clone()))
|
||||||
|
})
|
||||||
|
.chain(self.actions.iter().flat_map(|actions| {
|
||||||
|
actions
|
||||||
|
.iter()
|
||||||
|
.map(|action| CodeActionsItem::CodeAction(action.clone()))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
fn get(&self, index: usize) -> Option<CodeActionsItem> {
|
||||||
|
match (&self.tasks, &self.actions) {
|
||||||
|
(Some(tasks), Some(actions)) => {
|
||||||
|
if index < tasks.templates.len() {
|
||||||
|
tasks
|
||||||
|
.templates
|
||||||
|
.get(index)
|
||||||
|
.cloned()
|
||||||
|
.map(|(kind, task)| CodeActionsItem::Task(kind, task))
|
||||||
|
} else {
|
||||||
|
actions
|
||||||
|
.get(index - tasks.templates.len())
|
||||||
|
.cloned()
|
||||||
|
.map(CodeActionsItem::CodeAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Some(tasks), None) => tasks
|
||||||
|
.templates
|
||||||
|
.get(index)
|
||||||
|
.cloned()
|
||||||
|
.map(|(kind, task)| CodeActionsItem::Task(kind, task)),
|
||||||
|
(None, Some(actions)) => actions.get(index).cloned().map(CodeActionsItem::CodeAction),
|
||||||
|
(None, None) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum CodeActionsItem {
|
||||||
|
Task(TaskSourceKind, ResolvedTask),
|
||||||
|
CodeAction(CodeAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeActionsItem {
|
||||||
|
fn as_task(&self) -> Option<&ResolvedTask> {
|
||||||
|
let Self::Task(_, task) = self else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(task)
|
||||||
|
}
|
||||||
|
fn as_code_action(&self) -> Option<&CodeAction> {
|
||||||
|
let Self::CodeAction(action) = self else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
Some(action)
|
||||||
|
}
|
||||||
|
fn label(&self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::CodeAction(action) => action.lsp_action.title.clone(),
|
||||||
|
Self::Task(_, task) => task.resolved_label.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct CodeActionsMenu {
|
struct CodeActionsMenu {
|
||||||
actions: Arc<[CodeAction]>,
|
actions: CodeActionContents,
|
||||||
buffer: Model<Buffer>,
|
buffer: Model<Buffer>,
|
||||||
selected_item: usize,
|
selected_item: usize,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
deployed_from_indicator: bool,
|
deployed_from_indicator: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodeActionsMenu {
|
impl CodeActionsMenu {
|
||||||
|
@ -1240,8 +1352,10 @@ impl CodeActionsMenu {
|
||||||
"code_actions_menu",
|
"code_actions_menu",
|
||||||
self.actions.len(),
|
self.actions.len(),
|
||||||
move |_this, range, cx| {
|
move |_this, range, cx| {
|
||||||
actions[range.clone()]
|
actions
|
||||||
.iter()
|
.iter()
|
||||||
|
.skip(range.start)
|
||||||
|
.take(range.end - range.start)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, action)| {
|
.map(|(ix, action)| {
|
||||||
let item_ix = range.start + ix;
|
let item_ix = range.start + ix;
|
||||||
|
@ -1260,23 +1374,42 @@ impl CodeActionsMenu {
|
||||||
.bg(colors.element_hover)
|
.bg(colors.element_hover)
|
||||||
.text_color(colors.text_accent)
|
.text_color(colors.text_accent)
|
||||||
})
|
})
|
||||||
.on_mouse_down(
|
|
||||||
MouseButton::Left,
|
|
||||||
cx.listener(move |editor, _, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
if let Some(task) = editor.confirm_code_action(
|
|
||||||
&ConfirmCodeAction {
|
|
||||||
item_ix: Some(item_ix),
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
) {
|
|
||||||
task.detach_and_log_err(cx)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.whitespace_nowrap()
|
.whitespace_nowrap()
|
||||||
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
.when_some(action.as_code_action(), |this, action| {
|
||||||
.child(SharedString::from(action.lsp_action.title.clone()))
|
this.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |editor, _, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
if let Some(task) = editor.confirm_code_action(
|
||||||
|
&ConfirmCodeAction {
|
||||||
|
item_ix: Some(item_ix),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
task.detach_and_log_err(cx)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
|
||||||
|
.child(SharedString::from(action.lsp_action.title.clone()))
|
||||||
|
})
|
||||||
|
.when_some(action.as_task(), |this, task| {
|
||||||
|
this.on_mouse_down(
|
||||||
|
MouseButton::Left,
|
||||||
|
cx.listener(move |editor, _, cx| {
|
||||||
|
cx.stop_propagation();
|
||||||
|
if let Some(task) = editor.confirm_code_action(
|
||||||
|
&ConfirmCodeAction {
|
||||||
|
item_ix: Some(item_ix),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
) {
|
||||||
|
task.detach_and_log_err(cx)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(SharedString::from(task.resolved_label.clone()))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
},
|
},
|
||||||
|
@ -1291,16 +1424,20 @@ impl CodeActionsMenu {
|
||||||
self.actions
|
self.actions
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.max_by_key(|(_, action)| action.lsp_action.title.chars().count())
|
.max_by_key(|(_, action)| match action {
|
||||||
|
CodeActionsItem::Task(_, task) => task.resolved_label.chars().count(),
|
||||||
|
CodeActionsItem::CodeAction(action) => action.lsp_action.title.chars().count(),
|
||||||
|
})
|
||||||
.map(|(ix, _)| ix),
|
.map(|(ix, _)| ix),
|
||||||
)
|
)
|
||||||
.into_any_element();
|
.into_any_element();
|
||||||
|
|
||||||
let cursor_position = if self.deployed_from_indicator {
|
let cursor_position = if let Some(row) = self.deployed_from_indicator {
|
||||||
ContextMenuOrigin::GutterIndicator(cursor_position.row())
|
ContextMenuOrigin::GutterIndicator(row)
|
||||||
} else {
|
} else {
|
||||||
ContextMenuOrigin::EditorPoint(cursor_position)
|
ContextMenuOrigin::EditorPoint(cursor_position)
|
||||||
};
|
};
|
||||||
|
|
||||||
(cursor_position, element)
|
(cursor_position, element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1532,6 +1669,7 @@ impl Editor {
|
||||||
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
||||||
blame: None,
|
blame: None,
|
||||||
blame_subscription: None,
|
blame_subscription: None,
|
||||||
|
tasks: Default::default(),
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
cx.observe(&buffer, Self::on_buffer_changed),
|
cx.observe(&buffer, Self::on_buffer_changed),
|
||||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||||
|
@ -3687,38 +3825,131 @@ impl Editor {
|
||||||
|
|
||||||
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
|
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
|
||||||
let mut context_menu = self.context_menu.write();
|
let mut context_menu = self.context_menu.write();
|
||||||
if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
|
if let Some(ContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
|
||||||
*context_menu = None;
|
if code_actions.deployed_from_indicator == action.deployed_from_indicator {
|
||||||
cx.notify();
|
// Toggle if we're selecting the same one
|
||||||
return;
|
*context_menu = None;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Otherwise, clear it and start a new one
|
||||||
|
*context_menu = None;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
drop(context_menu);
|
drop(context_menu);
|
||||||
|
|
||||||
let deployed_from_indicator = action.deployed_from_indicator;
|
let deployed_from_indicator = action.deployed_from_indicator;
|
||||||
let mut task = self.code_actions_task.take();
|
let mut task = self.code_actions_task.take();
|
||||||
|
let action = action.clone();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
while let Some(prev_task) = task {
|
while let Some(prev_task) = task {
|
||||||
prev_task.await;
|
prev_task.await;
|
||||||
task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
|
task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
let spawned_test_task = this.update(&mut cx, |this, cx| {
|
||||||
if this.focus_handle.is_focused(cx) {
|
if this.focus_handle.is_focused(cx) {
|
||||||
if let Some((buffer, actions)) = this.available_code_actions.clone() {
|
let row = action
|
||||||
this.completion_tasks.clear();
|
.deployed_from_indicator
|
||||||
this.discard_inline_completion(cx);
|
.unwrap_or_else(|| this.selections.newest::<Point>(cx).head().row);
|
||||||
*this.context_menu.write() =
|
let tasks = this.tasks.get(&row).map(|t| Arc::new(t.to_owned()));
|
||||||
Some(ContextMenu::CodeActions(CodeActionsMenu {
|
let (buffer, code_actions) = this
|
||||||
buffer,
|
.available_code_actions
|
||||||
actions,
|
.clone()
|
||||||
selected_item: Default::default(),
|
.map(|(buffer, code_actions)| {
|
||||||
scroll_handle: UniformListScrollHandle::default(),
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
deployed_from_indicator,
|
let code_actions: Arc<[CodeAction]> = code_actions
|
||||||
}));
|
.into_iter()
|
||||||
cx.notify();
|
.filter(|action| {
|
||||||
|
text::ToPoint::to_point(&action.range.start, &snapshot).row
|
||||||
|
== row
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
(buffer, code_actions)
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
if tasks.is_none() && code_actions.is_none() {
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
let buffer = buffer.or_else(|| {
|
||||||
|
let snapshot = this.snapshot(cx);
|
||||||
|
let (buffer_snapshot, _) =
|
||||||
|
snapshot.buffer_snapshot.buffer_line_for_row(row)?;
|
||||||
|
let buffer_id = buffer_snapshot.remote_id();
|
||||||
|
this.buffer().read(cx).buffer(buffer_id)
|
||||||
|
});
|
||||||
|
let Some(buffer) = buffer else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
this.completion_tasks.clear();
|
||||||
|
this.discard_inline_completion(cx);
|
||||||
|
let task_context = tasks.as_ref().zip(this.workspace.clone()).and_then(
|
||||||
|
|(tasks, (workspace, _))| {
|
||||||
|
let position = Point::new(row, tasks.column);
|
||||||
|
let range_start = buffer.read(cx).anchor_at(position, Bias::Right);
|
||||||
|
let location = Location {
|
||||||
|
buffer: buffer.clone(),
|
||||||
|
range: range_start..range_start,
|
||||||
|
};
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
tasks::task_context_for_location(workspace, location, cx)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let tasks = tasks
|
||||||
|
.zip(task_context.as_ref())
|
||||||
|
.map(|(tasks, task_context)| {
|
||||||
|
Arc::new(ResolvedTasks {
|
||||||
|
templates: tasks
|
||||||
|
.templates
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(kind, template)| {
|
||||||
|
template
|
||||||
|
.resolve_task(&kind.to_id_base(), &task_context)
|
||||||
|
.map(|task| (kind.clone(), task))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
position: Point::new(row, tasks.column),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let spawn_straight_away = tasks
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |tasks| tasks.templates.len() == 1)
|
||||||
|
&& code_actions
|
||||||
|
.as_ref()
|
||||||
|
.map_or(true, |actions| actions.is_empty());
|
||||||
|
|
||||||
|
*this.context_menu.write() = Some(ContextMenu::CodeActions(CodeActionsMenu {
|
||||||
|
buffer,
|
||||||
|
actions: CodeActionContents {
|
||||||
|
tasks,
|
||||||
|
actions: code_actions,
|
||||||
|
},
|
||||||
|
selected_item: Default::default(),
|
||||||
|
scroll_handle: UniformListScrollHandle::default(),
|
||||||
|
deployed_from_indicator,
|
||||||
|
}));
|
||||||
|
if spawn_straight_away {
|
||||||
|
if let Some(task) =
|
||||||
|
this.confirm_code_action(&ConfirmCodeAction { item_ix: Some(0) }, cx)
|
||||||
|
{
|
||||||
|
cx.notify();
|
||||||
|
return Some(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
None
|
||||||
})?;
|
})?;
|
||||||
|
if let Some(task) = spawned_test_task {
|
||||||
|
task.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
})
|
})
|
||||||
|
@ -3736,23 +3967,47 @@ impl Editor {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
|
let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
|
||||||
let action = actions_menu.actions.get(action_ix)?.clone();
|
let action = actions_menu.actions.get(action_ix)?;
|
||||||
let title = action.lsp_action.title.clone();
|
let title = action.label();
|
||||||
let buffer = actions_menu.buffer;
|
let buffer = actions_menu.buffer;
|
||||||
let workspace = self.workspace()?;
|
let workspace = self.workspace()?;
|
||||||
|
|
||||||
let apply_code_actions = workspace
|
match action {
|
||||||
.read(cx)
|
CodeActionsItem::Task(task_source_kind, resolved_task) => {
|
||||||
.project()
|
workspace.update(cx, |workspace, cx| {
|
||||||
.clone()
|
workspace::tasks::schedule_resolved_task(
|
||||||
.update(cx, |project, cx| {
|
workspace,
|
||||||
project.apply_code_action(buffer, action, true, cx)
|
task_source_kind,
|
||||||
});
|
resolved_task,
|
||||||
let workspace = workspace.downgrade();
|
false,
|
||||||
Some(cx.spawn(|editor, cx| async move {
|
cx,
|
||||||
let project_transaction = apply_code_actions.await?;
|
);
|
||||||
Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await
|
|
||||||
}))
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
CodeActionsItem::CodeAction(action) => {
|
||||||
|
let apply_code_actions = workspace
|
||||||
|
.read(cx)
|
||||||
|
.project()
|
||||||
|
.clone()
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.apply_code_action(buffer, action, true, cx)
|
||||||
|
});
|
||||||
|
let workspace = workspace.downgrade();
|
||||||
|
Some(cx.spawn(|editor, cx| async move {
|
||||||
|
let project_transaction = apply_code_actions.await?;
|
||||||
|
Self::open_project_transaction(
|
||||||
|
&editor,
|
||||||
|
workspace,
|
||||||
|
project_transaction,
|
||||||
|
title,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn open_project_transaction(
|
async fn open_project_transaction(
|
||||||
|
@ -4213,9 +4468,10 @@ impl Editor {
|
||||||
Some(self.inline_completion_provider.as_ref()?.provider.clone())
|
Some(self.inline_completion_provider.as_ref()?.provider.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_code_actions_indicator(
|
fn render_code_actions_indicator(
|
||||||
&self,
|
&self,
|
||||||
_style: &EditorStyle,
|
_style: &EditorStyle,
|
||||||
|
row: u32,
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Option<IconButton> {
|
) -> Option<IconButton> {
|
||||||
|
@ -4226,10 +4482,10 @@ impl Editor {
|
||||||
.size(ui::ButtonSize::None)
|
.size(ui::ButtonSize::None)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.selected(is_active)
|
.selected(is_active)
|
||||||
.on_click(cx.listener(|editor, _e, cx| {
|
.on_click(cx.listener(move |editor, _e, cx| {
|
||||||
editor.toggle_code_actions(
|
editor.toggle_code_actions(
|
||||||
&ToggleCodeActions {
|
&ToggleCodeActions {
|
||||||
deployed_from_indicator: true,
|
deployed_from_indicator: Some(row),
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
@ -4240,6 +4496,39 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clear_tasks(&mut self) {
|
||||||
|
self.tasks.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_tasks(&mut self, row: u32, tasks: RunnableTasks) {
|
||||||
|
if let Some(_) = self.tasks.insert(row, tasks) {
|
||||||
|
// This case should hopefully be rare, but just in case...
|
||||||
|
log::error!("multiple different run targets found on a single line, only the last target will be rendered")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_run_indicator(
|
||||||
|
&self,
|
||||||
|
_style: &EditorStyle,
|
||||||
|
is_active: bool,
|
||||||
|
row: u32,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> IconButton {
|
||||||
|
IconButton::new("code_actions_indicator", ui::IconName::Play)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.size(ui::ButtonSize::None)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.selected(is_active)
|
||||||
|
.on_click(cx.listener(move |editor, _e, cx| {
|
||||||
|
editor.toggle_code_actions(
|
||||||
|
&ToggleCodeActions {
|
||||||
|
deployed_from_indicator: Some(row),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_fold_indicators(
|
pub fn render_fold_indicators(
|
||||||
&mut self,
|
&mut self,
|
||||||
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
|
fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
|
||||||
|
@ -7400,6 +7689,80 @@ impl Editor {
|
||||||
self.select_larger_syntax_node_stack = stack;
|
self.select_larger_syntax_node_stack = stack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runnable_display_rows(
|
||||||
|
&self,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
snapshot: &DisplaySnapshot,
|
||||||
|
cx: &WindowContext,
|
||||||
|
) -> Vec<(u32, RunnableTasks)> {
|
||||||
|
if self
|
||||||
|
.project
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |project| project.read(cx).is_remote())
|
||||||
|
{
|
||||||
|
// Do not display any test indicators in remote projects.
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.runnable_ranges(range)
|
||||||
|
.filter_map(|(multi_buffer_range, mut runnable)| {
|
||||||
|
let (tasks, _) = self.resolve_runnable(&mut runnable, cx);
|
||||||
|
if tasks.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let point = multi_buffer_range.start.to_display_point(&snapshot);
|
||||||
|
Some((
|
||||||
|
point.row(),
|
||||||
|
RunnableTasks {
|
||||||
|
templates: tasks,
|
||||||
|
column: point.column(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_runnable(
|
||||||
|
&self,
|
||||||
|
runnable: &mut Runnable,
|
||||||
|
cx: &WindowContext<'_>,
|
||||||
|
) -> (
|
||||||
|
SmallVec<[(TaskSourceKind, TaskTemplate); 1]>,
|
||||||
|
Option<WorktreeId>,
|
||||||
|
) {
|
||||||
|
let Some(project) = self.project.as_ref() else {
|
||||||
|
return Default::default();
|
||||||
|
};
|
||||||
|
let (inventory, worktree_id) = project.read_with(cx, |project, cx| {
|
||||||
|
let worktree_id = project
|
||||||
|
.buffer_for_id(runnable.buffer)
|
||||||
|
.and_then(|buffer| buffer.read(cx).file())
|
||||||
|
.map(|file| WorktreeId::from_usize(file.worktree_id()));
|
||||||
|
|
||||||
|
(project.task_inventory().clone(), worktree_id)
|
||||||
|
});
|
||||||
|
|
||||||
|
let inventory = inventory.read(cx);
|
||||||
|
let tags = mem::take(&mut runnable.tags);
|
||||||
|
(
|
||||||
|
SmallVec::from_iter(
|
||||||
|
tags.into_iter()
|
||||||
|
.flat_map(|tag| {
|
||||||
|
let tag = tag.0.clone();
|
||||||
|
inventory
|
||||||
|
.list_tasks(Some(runnable.language.clone()), worktree_id)
|
||||||
|
.into_iter()
|
||||||
|
.filter(move |(_, template)| {
|
||||||
|
template.tags.iter().any(|source_tag| source_tag == &tag)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.sorted_by_key(|(kind, _)| kind.to_owned()),
|
||||||
|
),
|
||||||
|
worktree_id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn move_to_enclosing_bracket(
|
pub fn move_to_enclosing_bracket(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &MoveToEnclosingBracket,
|
_: &MoveToEnclosingBracket,
|
||||||
|
|
|
@ -12,10 +12,11 @@ use crate::{
|
||||||
items::BufferSearchHighlights,
|
items::BufferSearchHighlights,
|
||||||
mouse_context_menu::{self, MouseContextMenu},
|
mouse_context_menu::{self, MouseContextMenu},
|
||||||
scroll::scroll_amount::ScrollAmount,
|
scroll::scroll_amount::ScrollAmount,
|
||||||
CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode,
|
CodeActionsMenu, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite,
|
||||||
EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, GutterDimensions, HalfPageDown,
|
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts,
|
||||||
HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp, OpenExcerpts, PageDown, PageUp,
|
GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HunkToExpand, LineDown, LineUp,
|
||||||
Point, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
OpenExcerpts, PageDown, PageUp, Point, RunnableTasks, SelectPhase, Selection, SoftWrap,
|
||||||
|
ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::ParticipantIndex;
|
use client::ParticipantIndex;
|
||||||
|
@ -1374,6 +1375,60 @@ impl EditorElement {
|
||||||
Some(shaped_lines)
|
Some(shaped_lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn layout_run_indicators(
|
||||||
|
&self,
|
||||||
|
task_lines: Vec<(u32, RunnableTasks)>,
|
||||||
|
line_height: Pixels,
|
||||||
|
scroll_pixel_position: gpui::Point<Pixels>,
|
||||||
|
gutter_dimensions: &GutterDimensions,
|
||||||
|
gutter_hitbox: &Hitbox,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Vec<AnyElement> {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.clear_tasks();
|
||||||
|
|
||||||
|
let active_task_indicator_row =
|
||||||
|
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
|
||||||
|
deployed_from_indicator,
|
||||||
|
actions,
|
||||||
|
..
|
||||||
|
})) = editor.context_menu.read().as_ref()
|
||||||
|
{
|
||||||
|
actions
|
||||||
|
.tasks
|
||||||
|
.as_ref()
|
||||||
|
.map(|tasks| tasks.position.row)
|
||||||
|
.or_else(|| *deployed_from_indicator)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
task_lines
|
||||||
|
.into_iter()
|
||||||
|
.map(|(row, tasks)| {
|
||||||
|
editor.insert_tasks(row, tasks);
|
||||||
|
|
||||||
|
let button = editor.render_run_indicator(
|
||||||
|
&self.style,
|
||||||
|
Some(row) == active_task_indicator_row,
|
||||||
|
row,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
let button = prepaint_gutter_button(
|
||||||
|
button,
|
||||||
|
row,
|
||||||
|
line_height,
|
||||||
|
gutter_dimensions,
|
||||||
|
scroll_pixel_position,
|
||||||
|
gutter_hitbox,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
button
|
||||||
|
})
|
||||||
|
.collect_vec()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn layout_code_actions_indicator(
|
fn layout_code_actions_indicator(
|
||||||
&self,
|
&self,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
|
@ -1385,35 +1440,28 @@ impl EditorElement {
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
let mut active = false;
|
let mut active = false;
|
||||||
let mut button = None;
|
let mut button = None;
|
||||||
|
let row = newest_selection_head.row();
|
||||||
self.editor.update(cx, |editor, cx| {
|
self.editor.update(cx, |editor, cx| {
|
||||||
active = matches!(
|
if let Some(crate::ContextMenu::CodeActions(CodeActionsMenu {
|
||||||
editor.context_menu.read().as_ref(),
|
deployed_from_indicator,
|
||||||
Some(crate::ContextMenu::CodeActions(_))
|
..
|
||||||
);
|
})) = editor.context_menu.read().as_ref()
|
||||||
button = editor.render_code_actions_indicator(&self.style, active, cx);
|
{
|
||||||
|
active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
|
||||||
|
};
|
||||||
|
button = editor.render_code_actions_indicator(&self.style, row, active, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut button = button?.into_any_element();
|
let button = prepaint_gutter_button(
|
||||||
let available_space = size(
|
button?,
|
||||||
AvailableSpace::MinContent,
|
row,
|
||||||
AvailableSpace::Definite(line_height),
|
line_height,
|
||||||
|
gutter_dimensions,
|
||||||
|
scroll_pixel_position,
|
||||||
|
gutter_hitbox,
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
let indicator_size = button.layout_as_root(available_space, cx);
|
|
||||||
|
|
||||||
let blame_width = gutter_dimensions
|
|
||||||
.git_blame_entries_width
|
|
||||||
.unwrap_or(Pixels::ZERO);
|
|
||||||
|
|
||||||
let mut x = blame_width;
|
|
||||||
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
|
||||||
- indicator_size.width
|
|
||||||
- blame_width;
|
|
||||||
x += available_width / 2.;
|
|
||||||
|
|
||||||
let mut y = newest_selection_head.row() as f32 * line_height - scroll_pixel_position.y;
|
|
||||||
y += (line_height - indicator_size.height) / 2.;
|
|
||||||
|
|
||||||
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
|
|
||||||
Some(button)
|
Some(button)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2351,6 +2399,10 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for test_indicators in layout.test_indicators.iter_mut() {
|
||||||
|
test_indicators.paint(cx);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
|
if let Some(indicator) = layout.code_actions_indicator.as_mut() {
|
||||||
indicator.paint(cx);
|
indicator.paint(cx);
|
||||||
}
|
}
|
||||||
|
@ -3224,6 +3276,39 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepaint_gutter_button(
|
||||||
|
button: IconButton,
|
||||||
|
row: u32,
|
||||||
|
line_height: Pixels,
|
||||||
|
gutter_dimensions: &GutterDimensions,
|
||||||
|
scroll_pixel_position: gpui::Point<Pixels>,
|
||||||
|
gutter_hitbox: &Hitbox,
|
||||||
|
cx: &mut WindowContext<'_>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let mut button = button.into_any_element();
|
||||||
|
let available_space = size(
|
||||||
|
AvailableSpace::MinContent,
|
||||||
|
AvailableSpace::Definite(line_height),
|
||||||
|
);
|
||||||
|
let indicator_size = button.layout_as_root(available_space, cx);
|
||||||
|
|
||||||
|
let blame_width = gutter_dimensions
|
||||||
|
.git_blame_entries_width
|
||||||
|
.unwrap_or(Pixels::ZERO);
|
||||||
|
|
||||||
|
let mut x = blame_width;
|
||||||
|
let available_width = gutter_dimensions.margin + gutter_dimensions.left_padding
|
||||||
|
- indicator_size.width
|
||||||
|
- blame_width;
|
||||||
|
x += available_width / 2.;
|
||||||
|
|
||||||
|
let mut y = row as f32 * line_height - scroll_pixel_position.y;
|
||||||
|
y += (line_height - indicator_size.height) / 2.;
|
||||||
|
|
||||||
|
button.prepaint_as_root(gutter_hitbox.origin + point(x, y), available_space, cx);
|
||||||
|
button
|
||||||
|
}
|
||||||
|
|
||||||
fn render_inline_blame_entry(
|
fn render_inline_blame_entry(
|
||||||
blame: &gpui::Model<GitBlame>,
|
blame: &gpui::Model<GitBlame>,
|
||||||
blame_entry: BlameEntry,
|
blame_entry: BlameEntry,
|
||||||
|
@ -3750,6 +3835,12 @@ impl Element for EditorElement {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let test_lines = self.editor.read(cx).runnable_display_rows(
|
||||||
|
start_anchor..end_anchor,
|
||||||
|
&snapshot.display_snapshot,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
let (selections, active_rows, newest_selection_head) = self.layout_selections(
|
let (selections, active_rows, newest_selection_head) = self.layout_selections(
|
||||||
start_anchor,
|
start_anchor,
|
||||||
end_anchor,
|
end_anchor,
|
||||||
|
@ -3939,18 +4030,32 @@ impl Element for EditorElement {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
if gutter_settings.code_actions {
|
if gutter_settings.code_actions {
|
||||||
code_actions_indicator = self.layout_code_actions_indicator(
|
let has_test_indicator = test_lines
|
||||||
line_height,
|
.iter()
|
||||||
newest_selection_head,
|
.any(|(line, _)| *line == newest_selection_head.row());
|
||||||
scroll_pixel_position,
|
if !has_test_indicator {
|
||||||
&gutter_dimensions,
|
code_actions_indicator = self.layout_code_actions_indicator(
|
||||||
&gutter_hitbox,
|
line_height,
|
||||||
cx,
|
newest_selection_head,
|
||||||
);
|
scroll_pixel_position,
|
||||||
|
&gutter_dimensions,
|
||||||
|
&gutter_hitbox,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let test_indicators = self.layout_run_indicators(
|
||||||
|
test_lines,
|
||||||
|
line_height,
|
||||||
|
scroll_pixel_position,
|
||||||
|
&gutter_dimensions,
|
||||||
|
&gutter_hitbox,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
if !context_menu_visible && !cx.has_active_drag() {
|
if !context_menu_visible && !cx.has_active_drag() {
|
||||||
self.layout_hover_popovers(
|
self.layout_hover_popovers(
|
||||||
&snapshot,
|
&snapshot,
|
||||||
|
@ -4051,6 +4156,7 @@ impl Element for EditorElement {
|
||||||
visible_cursors,
|
visible_cursors,
|
||||||
selections,
|
selections,
|
||||||
mouse_context_menu,
|
mouse_context_menu,
|
||||||
|
test_indicators,
|
||||||
code_actions_indicator,
|
code_actions_indicator,
|
||||||
fold_indicators,
|
fold_indicators,
|
||||||
tab_invisible,
|
tab_invisible,
|
||||||
|
@ -4170,6 +4276,7 @@ pub struct EditorLayout {
|
||||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||||
max_row: u32,
|
max_row: u32,
|
||||||
code_actions_indicator: Option<AnyElement>,
|
code_actions_indicator: Option<AnyElement>,
|
||||||
|
test_indicators: Vec<AnyElement>,
|
||||||
fold_indicators: Vec<Option<AnyElement>>,
|
fold_indicators: Vec<Option<AnyElement>>,
|
||||||
mouse_context_menu: Option<AnyElement>,
|
mouse_context_menu: Option<AnyElement>,
|
||||||
tab_invisible: ShapedLine,
|
tab_invisible: ShapedLine,
|
||||||
|
|
|
@ -79,7 +79,7 @@ pub fn deploy_context_menu(
|
||||||
.action(
|
.action(
|
||||||
"Code Actions",
|
"Code Actions",
|
||||||
Box::new(ToggleCodeActions {
|
Box::new(ToggleCodeActions {
|
||||||
deployed_from_indicator: false,
|
deployed_from_indicator: None,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
|
|
118
crates/editor/src/tasks.rs
Normal file
118
crates/editor/src/tasks.rs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
use crate::Editor;
|
||||||
|
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use gpui::WindowContext;
|
||||||
|
use language::{BasicContextProvider, ContextProvider};
|
||||||
|
use project::{Location, WorktreeId};
|
||||||
|
use task::{TaskContext, TaskVariables};
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
pub(crate) fn task_context_for_location(
|
||||||
|
workspace: &Workspace,
|
||||||
|
location: Location,
|
||||||
|
cx: &mut WindowContext<'_>,
|
||||||
|
) -> Option<TaskContext> {
|
||||||
|
let cwd = workspace::tasks::task_cwd(workspace, cx)
|
||||||
|
.log_err()
|
||||||
|
.flatten();
|
||||||
|
|
||||||
|
let buffer = location.buffer.clone();
|
||||||
|
let language_context_provider = buffer
|
||||||
|
.read(cx)
|
||||||
|
.language()
|
||||||
|
.and_then(|language| language.context_provider())
|
||||||
|
.unwrap_or_else(|| Arc::new(BasicContextProvider));
|
||||||
|
|
||||||
|
let worktree_abs_path = buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.map(|file| WorktreeId::from_usize(file.worktree_id()))
|
||||||
|
.and_then(|worktree_id| {
|
||||||
|
workspace
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.worktree_for_id(worktree_id, cx)
|
||||||
|
.map(|worktree| worktree.read(cx).abs_path())
|
||||||
|
});
|
||||||
|
let task_variables = combine_task_variables(
|
||||||
|
worktree_abs_path.as_deref(),
|
||||||
|
location,
|
||||||
|
language_context_provider.as_ref(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.log_err()?;
|
||||||
|
Some(TaskContext {
|
||||||
|
cwd,
|
||||||
|
task_variables,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn task_context_with_editor(
|
||||||
|
workspace: &Workspace,
|
||||||
|
editor: &mut Editor,
|
||||||
|
cx: &mut WindowContext<'_>,
|
||||||
|
) -> Option<TaskContext> {
|
||||||
|
let (selection, buffer, editor_snapshot) = {
|
||||||
|
let selection = editor.selections.newest::<usize>(cx);
|
||||||
|
let (buffer, _, _) = editor
|
||||||
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.point_to_buffer_offset(selection.start, cx)?;
|
||||||
|
let snapshot = editor.snapshot(cx);
|
||||||
|
Some((selection, buffer, snapshot))
|
||||||
|
}?;
|
||||||
|
let selection_range = selection.range();
|
||||||
|
let start = editor_snapshot
|
||||||
|
.display_snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.anchor_after(selection_range.start)
|
||||||
|
.text_anchor;
|
||||||
|
let end = editor_snapshot
|
||||||
|
.display_snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.anchor_after(selection_range.end)
|
||||||
|
.text_anchor;
|
||||||
|
let location = Location {
|
||||||
|
buffer,
|
||||||
|
range: start..end,
|
||||||
|
};
|
||||||
|
task_context_for_location(workspace, location, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
|
||||||
|
let Some(editor) = workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.and_then(|item| item.act_as::<Editor>(cx))
|
||||||
|
else {
|
||||||
|
return Default::default();
|
||||||
|
};
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
task_context_with_editor(workspace, editor, cx).unwrap_or_default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn combine_task_variables(
|
||||||
|
worktree_abs_path: Option<&Path>,
|
||||||
|
location: Location,
|
||||||
|
context_provider: &dyn ContextProvider,
|
||||||
|
cx: &mut WindowContext<'_>,
|
||||||
|
) -> anyhow::Result<TaskVariables> {
|
||||||
|
if context_provider.is_basic() {
|
||||||
|
context_provider
|
||||||
|
.build_context(worktree_abs_path, &location, cx)
|
||||||
|
.context("building basic provider context")
|
||||||
|
} else {
|
||||||
|
let mut basic_context = BasicContextProvider
|
||||||
|
.build_context(worktree_abs_path, &location, cx)
|
||||||
|
.context("building basic default context")?;
|
||||||
|
basic_context.extend(
|
||||||
|
context_provider
|
||||||
|
.build_context(worktree_abs_path, &location, cx)
|
||||||
|
.context("building provider context ")?,
|
||||||
|
);
|
||||||
|
Ok(basic_context)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ use crate::{
|
||||||
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
|
||||||
SyntaxSnapshot, ToTreeSitterPoint,
|
SyntaxSnapshot, ToTreeSitterPoint,
|
||||||
},
|
},
|
||||||
LanguageScope, Outline,
|
LanguageScope, Outline, RunnableTag,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
pub use clock::ReplicaId;
|
pub use clock::ReplicaId;
|
||||||
|
@ -501,6 +501,13 @@ pub enum CharKind {
|
||||||
Word,
|
Word,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A runnable is a set of data about a region that could be resolved into a task
|
||||||
|
pub struct Runnable {
|
||||||
|
pub tags: SmallVec<[RunnableTag; 1]>,
|
||||||
|
pub language: Arc<Language>,
|
||||||
|
pub buffer: BufferId,
|
||||||
|
}
|
||||||
|
|
||||||
impl Buffer {
|
impl Buffer {
|
||||||
/// Create a new buffer with the given base text.
|
/// Create a new buffer with the given base text.
|
||||||
pub fn local<T: Into<String>>(base_text: T, cx: &mut ModelContext<Self>) -> Self {
|
pub fn local<T: Into<String>>(base_text: T, cx: &mut ModelContext<Self>) -> Self {
|
||||||
|
@ -2978,6 +2985,53 @@ impl BufferSnapshot {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runnable_ranges(
|
||||||
|
&self,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
|
||||||
|
let offset_range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
|
||||||
|
let mut syntax_matches = self.syntax.matches(offset_range, self, |grammar| {
|
||||||
|
grammar.runnable_config.as_ref().map(|config| &config.query)
|
||||||
|
});
|
||||||
|
|
||||||
|
let test_configs = syntax_matches
|
||||||
|
.grammars()
|
||||||
|
.iter()
|
||||||
|
.map(|grammar| grammar.runnable_config.as_ref())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
iter::from_fn(move || {
|
||||||
|
let test_range = syntax_matches
|
||||||
|
.peek()
|
||||||
|
.and_then(|mat| {
|
||||||
|
test_configs[mat.grammar_index].and_then(|test_configs| {
|
||||||
|
let tags = SmallVec::from_iter(mat.captures.iter().filter_map(|capture| {
|
||||||
|
test_configs.runnable_tags.get(&capture.index).cloned()
|
||||||
|
}));
|
||||||
|
|
||||||
|
if tags.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((
|
||||||
|
mat.captures
|
||||||
|
.iter()
|
||||||
|
.find(|capture| capture.index == test_configs.run_capture_ix)?,
|
||||||
|
Runnable {
|
||||||
|
tags,
|
||||||
|
language: mat.language,
|
||||||
|
buffer: self.remote_id(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.map(|(mat, test_tags)| (mat.node.byte_range(), test_tags));
|
||||||
|
syntax_matches.advance();
|
||||||
|
test_range
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns selections for remote peers intersecting the given range.
|
/// Returns selections for remote peers intersecting the given range.
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
pub fn remote_selections_in_range(
|
pub fn remote_selections_in_range(
|
||||||
|
|
|
@ -56,6 +56,7 @@ use std::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
|
use syntax_map::{QueryCursorHandle, SyntaxSnapshot};
|
||||||
|
use task::RunnableTag;
|
||||||
pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
|
pub use task_context::{BasicContextProvider, ContextProvider, ContextProviderWithTasks};
|
||||||
use theme::SyntaxTheme;
|
use theme::SyntaxTheme;
|
||||||
use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
|
use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore};
|
||||||
|
@ -836,6 +837,7 @@ pub struct Grammar {
|
||||||
pub(crate) highlights_query: Option<Query>,
|
pub(crate) highlights_query: Option<Query>,
|
||||||
pub(crate) brackets_config: Option<BracketConfig>,
|
pub(crate) brackets_config: Option<BracketConfig>,
|
||||||
pub(crate) redactions_config: Option<RedactionConfig>,
|
pub(crate) redactions_config: Option<RedactionConfig>,
|
||||||
|
pub(crate) runnable_config: Option<RunnableConfig>,
|
||||||
pub(crate) indents_config: Option<IndentConfig>,
|
pub(crate) indents_config: Option<IndentConfig>,
|
||||||
pub outline_config: Option<OutlineConfig>,
|
pub outline_config: Option<OutlineConfig>,
|
||||||
pub embedding_config: Option<EmbeddingConfig>,
|
pub embedding_config: Option<EmbeddingConfig>,
|
||||||
|
@ -882,6 +884,14 @@ struct RedactionConfig {
|
||||||
pub redaction_capture_ix: u32,
|
pub redaction_capture_ix: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RunnableConfig {
|
||||||
|
pub query: Query,
|
||||||
|
/// A mapping from captures indices to known test tags
|
||||||
|
pub runnable_tags: HashMap<u32, RunnableTag>,
|
||||||
|
/// index of the capture that corresponds to @run
|
||||||
|
pub run_capture_ix: u32,
|
||||||
|
}
|
||||||
|
|
||||||
struct OverrideConfig {
|
struct OverrideConfig {
|
||||||
query: Query,
|
query: Query,
|
||||||
values: HashMap<u32, (String, LanguageConfigOverride)>,
|
values: HashMap<u32, (String, LanguageConfigOverride)>,
|
||||||
|
@ -923,6 +933,7 @@ impl Language {
|
||||||
injection_config: None,
|
injection_config: None,
|
||||||
override_config: None,
|
override_config: None,
|
||||||
redactions_config: None,
|
redactions_config: None,
|
||||||
|
runnable_config: None,
|
||||||
error_query: Query::new(&ts_language, "(ERROR) @error").unwrap(),
|
error_query: Query::new(&ts_language, "(ERROR) @error").unwrap(),
|
||||||
ts_language,
|
ts_language,
|
||||||
highlight_map: Default::default(),
|
highlight_map: Default::default(),
|
||||||
|
@ -978,6 +989,11 @@ impl Language {
|
||||||
.with_redaction_query(query.as_ref())
|
.with_redaction_query(query.as_ref())
|
||||||
.context("Error loading redaction query")?;
|
.context("Error loading redaction query")?;
|
||||||
}
|
}
|
||||||
|
if let Some(query) = queries.runnables {
|
||||||
|
self = self
|
||||||
|
.with_runnable_query(query.as_ref())
|
||||||
|
.context("Error loading tests query")?;
|
||||||
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -989,6 +1005,33 @@ impl Language {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_runnable_query(mut self, source: &str) -> Result<Self> {
|
||||||
|
let grammar = self
|
||||||
|
.grammar_mut()
|
||||||
|
.ok_or_else(|| anyhow!("cannot mutate grammar"))?;
|
||||||
|
|
||||||
|
let query = Query::new(&grammar.ts_language, source)?;
|
||||||
|
let mut run_capture_index = None;
|
||||||
|
let mut runnable_tags = HashMap::default();
|
||||||
|
for (ix, name) in query.capture_names().iter().enumerate() {
|
||||||
|
if *name == "run" {
|
||||||
|
run_capture_index = Some(ix as u32);
|
||||||
|
} else if !name.starts_with('_') {
|
||||||
|
runnable_tags.insert(ix as u32, RunnableTag(name.to_string().into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(run_capture_ix) = run_capture_index {
|
||||||
|
grammar.runnable_config = Some(RunnableConfig {
|
||||||
|
query,
|
||||||
|
run_capture_ix,
|
||||||
|
runnable_tags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
|
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
|
||||||
let grammar = self
|
let grammar = self
|
||||||
.grammar_mut()
|
.grammar_mut()
|
||||||
|
|
|
@ -124,6 +124,7 @@ pub const QUERY_FILENAME_PREFIXES: &[(
|
||||||
("injections", |q| &mut q.injections),
|
("injections", |q| &mut q.injections),
|
||||||
("overrides", |q| &mut q.overrides),
|
("overrides", |q| &mut q.overrides),
|
||||||
("redactions", |q| &mut q.redactions),
|
("redactions", |q| &mut q.redactions),
|
||||||
|
("runnables", |q| &mut q.runnables),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Tree-sitter language queries for a given language.
|
/// Tree-sitter language queries for a given language.
|
||||||
|
@ -137,6 +138,7 @@ pub struct LanguageQueries {
|
||||||
pub injections: Option<Cow<'static, str>>,
|
pub injections: Option<Cow<'static, str>>,
|
||||||
pub overrides: Option<Cow<'static, str>>,
|
pub overrides: Option<Cow<'static, str>>,
|
||||||
pub redactions: Option<Cow<'static, str>>,
|
pub redactions: Option<Cow<'static, str>>,
|
||||||
|
pub runnables: Option<Cow<'static, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
|
|
|
@ -56,6 +56,7 @@ pub struct SyntaxMapCapture<'a> {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SyntaxMapMatch<'a> {
|
pub struct SyntaxMapMatch<'a> {
|
||||||
|
pub language: Arc<Language>,
|
||||||
pub depth: usize,
|
pub depth: usize,
|
||||||
pub pattern_index: usize,
|
pub pattern_index: usize,
|
||||||
pub captures: &'a [QueryCapture<'a>],
|
pub captures: &'a [QueryCapture<'a>],
|
||||||
|
@ -71,6 +72,7 @@ struct SyntaxMapCapturesLayer<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SyntaxMapMatchesLayer<'a> {
|
struct SyntaxMapMatchesLayer<'a> {
|
||||||
|
language: Arc<Language>,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
next_pattern_index: usize,
|
next_pattern_index: usize,
|
||||||
next_captures: Vec<QueryCapture<'a>>,
|
next_captures: Vec<QueryCapture<'a>>,
|
||||||
|
@ -1016,6 +1018,7 @@ impl<'a> SyntaxMapMatches<'a> {
|
||||||
result.grammars.len() - 1
|
result.grammars.len() - 1
|
||||||
});
|
});
|
||||||
let mut layer = SyntaxMapMatchesLayer {
|
let mut layer = SyntaxMapMatchesLayer {
|
||||||
|
language: layer.language.clone(),
|
||||||
depth: layer.depth,
|
depth: layer.depth,
|
||||||
grammar_index,
|
grammar_index,
|
||||||
matches,
|
matches,
|
||||||
|
@ -1048,10 +1051,13 @@ impl<'a> SyntaxMapMatches<'a> {
|
||||||
|
|
||||||
pub fn peek(&self) -> Option<SyntaxMapMatch> {
|
pub fn peek(&self) -> Option<SyntaxMapMatch> {
|
||||||
let layer = self.layers.first()?;
|
let layer = self.layers.first()?;
|
||||||
|
|
||||||
if !layer.has_next {
|
if !layer.has_next {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(SyntaxMapMatch {
|
Some(SyntaxMapMatch {
|
||||||
|
language: layer.language.clone(),
|
||||||
depth: layer.depth,
|
depth: layer.depth,
|
||||||
grammar_index: layer.grammar_index,
|
grammar_index: layer.grammar_index,
|
||||||
pattern_index: layer.next_pattern_index,
|
pattern_index: layer.next_pattern_index,
|
||||||
|
|
|
@ -389,6 +389,7 @@ impl ContextProvider for RustContextProvider {
|
||||||
"--".into(),
|
"--".into(),
|
||||||
"--nocapture".into(),
|
"--nocapture".into(),
|
||||||
],
|
],
|
||||||
|
tags: vec!["rust-test".to_owned()],
|
||||||
..TaskTemplate::default()
|
..TaskTemplate::default()
|
||||||
},
|
},
|
||||||
TaskTemplate {
|
TaskTemplate {
|
||||||
|
|
7
crates/languages/src/rust/runnables.scm
Normal file
7
crates/languages/src/rust/runnables.scm
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
(
|
||||||
|
(attribute_item (attribute) @_attribute
|
||||||
|
(#match? @_attribute ".*test.*"))
|
||||||
|
.
|
||||||
|
(function_item
|
||||||
|
name: (_) @run)
|
||||||
|
) @rust-test
|
|
@ -13,7 +13,7 @@ use language::{
|
||||||
language_settings::{language_settings, LanguageSettings},
|
language_settings::{language_settings, LanguageSettings},
|
||||||
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape,
|
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape,
|
||||||
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
|
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
|
||||||
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
|
Outline, OutlineItem, Point, PointUtf16, Runnable, Selection, TextDimension, ToOffset as _,
|
||||||
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
@ -3165,6 +3165,31 @@ impl MultiBufferSnapshot {
|
||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runnable_ranges(
|
||||||
|
&self,
|
||||||
|
range: Range<Anchor>,
|
||||||
|
) -> impl Iterator<Item = (Range<usize>, Runnable)> + '_ {
|
||||||
|
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||||
|
self.excerpts_for_range(range.clone())
|
||||||
|
.flat_map(move |(excerpt, excerpt_offset)| {
|
||||||
|
let excerpt_buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
|
||||||
|
|
||||||
|
excerpt
|
||||||
|
.buffer
|
||||||
|
.runnable_ranges(excerpt.range.context.clone())
|
||||||
|
.map(move |(mut match_range, runnable)| {
|
||||||
|
// Re-base onto the excerpts coordinates in the multibuffer
|
||||||
|
match_range.start =
|
||||||
|
excerpt_offset + (match_range.start - excerpt_buffer_start);
|
||||||
|
match_range.end = excerpt_offset + (match_range.end - excerpt_buffer_start);
|
||||||
|
|
||||||
|
(match_range, runnable)
|
||||||
|
})
|
||||||
|
.skip_while(move |(match_range, _)| match_range.end < range.start)
|
||||||
|
.take_while(move |(match_range, _)| match_range.start < range.end)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn diagnostics_update_count(&self) -> usize {
|
pub fn diagnostics_update_count(&self) -> usize {
|
||||||
self.diagnostics_update_count
|
self.diagnostics_update_count
|
||||||
}
|
}
|
||||||
|
|
|
@ -7655,17 +7655,15 @@ impl Project {
|
||||||
} else {
|
} else {
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let task_abs_path = abs_path.clone();
|
let task_abs_path = abs_path.clone();
|
||||||
|
let tasks_file_rx =
|
||||||
|
watch_config_file(&cx.background_executor(), fs, task_abs_path);
|
||||||
task_inventory.add_source(
|
task_inventory.add_source(
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id: remote_worktree_id,
|
id: remote_worktree_id,
|
||||||
abs_path,
|
abs_path,
|
||||||
id_base: "local_tasks_for_worktree",
|
id_base: "local_tasks_for_worktree",
|
||||||
},
|
},
|
||||||
|cx| {
|
StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
|
||||||
let tasks_file_rx =
|
|
||||||
watch_config_file(&cx.background_executor(), fs, task_abs_path);
|
|
||||||
StaticSource::new(TrackedFile::new(tasks_file_rx, cx), cx)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7677,23 +7675,20 @@ impl Project {
|
||||||
} else {
|
} else {
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
let task_abs_path = abs_path.clone();
|
let task_abs_path = abs_path.clone();
|
||||||
|
let tasks_file_rx =
|
||||||
|
watch_config_file(&cx.background_executor(), fs, task_abs_path);
|
||||||
task_inventory.add_source(
|
task_inventory.add_source(
|
||||||
TaskSourceKind::Worktree {
|
TaskSourceKind::Worktree {
|
||||||
id: remote_worktree_id,
|
id: remote_worktree_id,
|
||||||
abs_path,
|
abs_path,
|
||||||
id_base: "local_vscode_tasks_for_worktree",
|
id_base: "local_vscode_tasks_for_worktree",
|
||||||
},
|
},
|
||||||
|cx| {
|
StaticSource::new(
|
||||||
let tasks_file_rx =
|
TrackedFile::new_convertible::<task::VsCodeTaskFile>(
|
||||||
watch_config_file(&cx.background_executor(), fs, task_abs_path);
|
tasks_file_rx,
|
||||||
StaticSource::new(
|
|
||||||
TrackedFile::new_convertible::<task::VsCodeTaskFile>(
|
|
||||||
tasks_file_rx,
|
|
||||||
cx,
|
|
||||||
),
|
|
||||||
cx,
|
cx,
|
||||||
)
|
),
|
||||||
},
|
),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ use serde_json::json;
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use std::os;
|
use std::os;
|
||||||
use std::task::Poll;
|
use std::task::Poll;
|
||||||
use task::{TaskContext, TaskSource, TaskTemplate, TaskTemplates};
|
use task::{TaskContext, TaskTemplate, TaskTemplates};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
|
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
|
||||||
use worktree::WorktreeModelHandle as _;
|
use worktree::WorktreeModelHandle as _;
|
||||||
|
@ -168,12 +168,11 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||||
|
|
||||||
let all_tasks = project
|
let all_tasks = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.task_inventory().update(cx, |inventory, cx| {
|
project.task_inventory().update(cx, |inventory, _| {
|
||||||
let (mut old, new) = inventory.used_and_current_resolved_tasks(
|
let (mut old, new) = inventory.used_and_current_resolved_tasks(
|
||||||
None,
|
None,
|
||||||
Some(workree_id),
|
Some(workree_id),
|
||||||
&task_context,
|
&task_context,
|
||||||
cx,
|
|
||||||
);
|
);
|
||||||
old.extend(new);
|
old.extend(new);
|
||||||
old
|
old
|
||||||
|
@ -215,13 +214,9 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||||
|
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
let inventory = project.task_inventory();
|
let inventory = project.task_inventory();
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, _| {
|
||||||
let (mut old, new) = inventory.used_and_current_resolved_tasks(
|
let (mut old, new) =
|
||||||
None,
|
inventory.used_and_current_resolved_tasks(None, Some(workree_id), &task_context);
|
||||||
Some(workree_id),
|
|
||||||
&task_context,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
old.extend(new);
|
old.extend(new);
|
||||||
let (_, resolved_task) = old
|
let (_, resolved_task) = old
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -231,41 +226,39 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let tasks = serde_json::to_string(&TaskTemplates(vec![TaskTemplate {
|
||||||
|
label: "cargo check".to_string(),
|
||||||
|
command: "cargo".to_string(),
|
||||||
|
args: vec![
|
||||||
|
"check".to_string(),
|
||||||
|
"--all".to_string(),
|
||||||
|
"--all-targets".to_string(),
|
||||||
|
],
|
||||||
|
env: HashMap::from_iter(Some((
|
||||||
|
"RUSTFLAGS".to_string(),
|
||||||
|
"-Zunstable-options".to_string(),
|
||||||
|
))),
|
||||||
|
..TaskTemplate::default()
|
||||||
|
}]))
|
||||||
|
.unwrap();
|
||||||
|
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||||
|
|
||||||
|
let templates = cx.update(|cx| TrackedFile::new(rx, cx));
|
||||||
|
tx.unbounded_send(tasks).unwrap();
|
||||||
|
|
||||||
|
let source = StaticSource::new(templates);
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
let all_tasks = project
|
let all_tasks = project
|
||||||
.update(cx, |project, cx| {
|
.update(cx, |project, cx| {
|
||||||
project.task_inventory().update(cx, |inventory, cx| {
|
project.task_inventory().update(cx, |inventory, cx| {
|
||||||
inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
|
inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
|
||||||
inventory.add_source(
|
inventory.add_source(global_task_source_kind.clone(), source, cx);
|
||||||
global_task_source_kind.clone(),
|
|
||||||
|cx| {
|
|
||||||
cx.new_model(|_| {
|
|
||||||
let source = TestTaskSource {
|
|
||||||
tasks: TaskTemplates(vec![TaskTemplate {
|
|
||||||
label: "cargo check".to_string(),
|
|
||||||
command: "cargo".to_string(),
|
|
||||||
args: vec![
|
|
||||||
"check".to_string(),
|
|
||||||
"--all".to_string(),
|
|
||||||
"--all-targets".to_string(),
|
|
||||||
],
|
|
||||||
env: HashMap::from_iter(Some((
|
|
||||||
"RUSTFLAGS".to_string(),
|
|
||||||
"-Zunstable-options".to_string(),
|
|
||||||
))),
|
|
||||||
..TaskTemplate::default()
|
|
||||||
}]),
|
|
||||||
};
|
|
||||||
Box::new(source) as Box<_>
|
|
||||||
})
|
|
||||||
},
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let (mut old, new) = inventory.used_and_current_resolved_tasks(
|
let (mut old, new) = inventory.used_and_current_resolved_tasks(
|
||||||
None,
|
None,
|
||||||
Some(workree_id),
|
Some(workree_id),
|
||||||
&task_context,
|
&task_context,
|
||||||
cx,
|
|
||||||
);
|
);
|
||||||
old.extend(new);
|
old.extend(new);
|
||||||
old
|
old
|
||||||
|
@ -317,20 +310,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestTaskSource {
|
|
||||||
tasks: TaskTemplates,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskSource for TestTaskSource {
|
|
||||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
|
|
||||||
self.tasks.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
//! 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::{
|
use std::{
|
||||||
any::TypeId,
|
|
||||||
cmp::{self, Reverse},
|
cmp::{self, Reverse},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use collections::{hash_map, HashMap, VecDeque};
|
use collections::{hash_map, HashMap, VecDeque};
|
||||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
use gpui::{AppContext, Context, Model, ModelContext};
|
||||||
use itertools::{Either, Itertools};
|
use itertools::{Either, Itertools};
|
||||||
use language::Language;
|
use language::Language;
|
||||||
use task::{ResolvedTask, TaskContext, TaskId, TaskSource, TaskTemplate, VariableName};
|
use task::{
|
||||||
|
static_source::StaticSource, ResolvedTask, TaskContext, TaskId, TaskTemplate, VariableName,
|
||||||
|
};
|
||||||
use util::{post_inc, NumericPrefixWithSuffix};
|
use util::{post_inc, NumericPrefixWithSuffix};
|
||||||
use worktree::WorktreeId;
|
use worktree::WorktreeId;
|
||||||
|
|
||||||
|
@ -22,14 +23,12 @@ pub struct Inventory {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SourceInInventory {
|
struct SourceInInventory {
|
||||||
source: Model<Box<dyn TaskSource>>,
|
source: StaticSource,
|
||||||
_subscription: Subscription,
|
|
||||||
type_id: TypeId,
|
|
||||||
kind: TaskSourceKind,
|
kind: TaskSourceKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
|
/// Kind of a source the tasks are fetched from, used to display more source information in the UI.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub enum TaskSourceKind {
|
pub enum TaskSourceKind {
|
||||||
/// bash-like commands spawned by users, not associated with any path
|
/// bash-like commands spawned by users, not associated with any path
|
||||||
UserInput,
|
UserInput,
|
||||||
|
@ -95,7 +94,7 @@ impl Inventory {
|
||||||
pub fn add_source(
|
pub fn add_source(
|
||||||
&mut self,
|
&mut self,
|
||||||
kind: TaskSourceKind,
|
kind: TaskSourceKind,
|
||||||
create_source: impl FnOnce(&mut ModelContext<Self>) -> Model<Box<dyn TaskSource>>,
|
source: StaticSource,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
let abs_path = kind.abs_path();
|
let abs_path = kind.abs_path();
|
||||||
|
@ -106,16 +105,7 @@ impl Inventory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = create_source(cx);
|
let source = SourceInInventory { source, kind };
|
||||||
let type_id = source.read(cx).type_id();
|
|
||||||
let source = SourceInInventory {
|
|
||||||
_subscription: cx.observe(&source, |_, _, cx| {
|
|
||||||
cx.notify();
|
|
||||||
}),
|
|
||||||
source,
|
|
||||||
type_id,
|
|
||||||
kind,
|
|
||||||
};
|
|
||||||
self.sources.push(source);
|
self.sources.push(source);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -136,31 +126,12 @@ impl Inventory {
|
||||||
self.sources.retain(|s| s.kind.worktree() != Some(worktree));
|
self.sources.retain(|s| s.kind.worktree() != Some(worktree));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn source<T: TaskSource>(&self) -> Option<(Model<Box<dyn TaskSource>>, TaskSourceKind)> {
|
|
||||||
let target_type_id = std::any::TypeId::of::<T>();
|
|
||||||
self.sources.iter().find_map(
|
|
||||||
|SourceInInventory {
|
|
||||||
type_id,
|
|
||||||
source,
|
|
||||||
kind,
|
|
||||||
..
|
|
||||||
}| {
|
|
||||||
if &target_type_id == type_id {
|
|
||||||
Some((source.clone(), kind.clone()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pulls its task sources relevant to the worktree and the language given,
|
/// Pulls its task sources relevant to the worktree and the language given,
|
||||||
/// returns all task templates with their source kinds, in no specific order.
|
/// returns all task templates with their source kinds, in no specific order.
|
||||||
pub fn list_tasks(
|
pub fn list_tasks(
|
||||||
&self,
|
&self,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
cx: &mut AppContext,
|
|
||||||
) -> Vec<(TaskSourceKind, TaskTemplate)> {
|
) -> Vec<(TaskSourceKind, TaskTemplate)> {
|
||||||
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
|
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
|
||||||
name: language.name(),
|
name: language.name(),
|
||||||
|
@ -180,7 +151,7 @@ impl Inventory {
|
||||||
.flat_map(|source| {
|
.flat_map(|source| {
|
||||||
source
|
source
|
||||||
.source
|
.source
|
||||||
.update(cx, |source, cx| source.tasks_to_schedule(cx))
|
.tasks_to_schedule()
|
||||||
.0
|
.0
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|task| (&source.kind, task))
|
.map(|task| (&source.kind, task))
|
||||||
|
@ -199,7 +170,6 @@ impl Inventory {
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
task_context: &TaskContext,
|
task_context: &TaskContext,
|
||||||
cx: &mut AppContext,
|
|
||||||
) -> (
|
) -> (
|
||||||
Vec<(TaskSourceKind, ResolvedTask)>,
|
Vec<(TaskSourceKind, ResolvedTask)>,
|
||||||
Vec<(TaskSourceKind, ResolvedTask)>,
|
Vec<(TaskSourceKind, ResolvedTask)>,
|
||||||
|
@ -246,7 +216,7 @@ impl Inventory {
|
||||||
.flat_map(|source| {
|
.flat_map(|source| {
|
||||||
source
|
source
|
||||||
.source
|
.source
|
||||||
.update(cx, |source, cx| source.tasks_to_schedule(cx))
|
.tasks_to_schedule()
|
||||||
.0
|
.0
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|task| (&source.kind, task))
|
.map(|task| (&source.kind, task))
|
||||||
|
@ -387,9 +357,12 @@ fn task_variables_preference(task: &ResolvedTask) -> Reverse<usize> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_inventory {
|
mod test_inventory {
|
||||||
use gpui::{AppContext, Context as _, Model, ModelContext, TestAppContext};
|
use gpui::{AppContext, Model, TestAppContext};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use task::{TaskContext, TaskId, TaskSource, TaskTemplate, TaskTemplates};
|
use task::{
|
||||||
|
static_source::{StaticSource, TrackedFile},
|
||||||
|
TaskContext, TaskTemplate, TaskTemplates,
|
||||||
|
};
|
||||||
use worktree::WorktreeId;
|
use worktree::WorktreeId;
|
||||||
|
|
||||||
use crate::Inventory;
|
use crate::Inventory;
|
||||||
|
@ -398,55 +371,28 @@ mod test_inventory {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TestTask {
|
pub struct TestTask {
|
||||||
id: task::TaskId,
|
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StaticTestSource {
|
pub(super) fn static_test_source(
|
||||||
pub tasks: Vec<TestTask>,
|
task_names: impl IntoIterator<Item = String>,
|
||||||
}
|
cx: &mut AppContext,
|
||||||
|
) -> StaticSource {
|
||||||
impl StaticTestSource {
|
let tasks = TaskTemplates(
|
||||||
pub(super) fn new(
|
task_names
|
||||||
task_names: impl IntoIterator<Item = String>,
|
.into_iter()
|
||||||
cx: &mut AppContext,
|
.map(|name| TaskTemplate {
|
||||||
) -> Model<Box<dyn TaskSource>> {
|
label: name,
|
||||||
cx.new_model(|_| {
|
command: "test command".to_owned(),
|
||||||
Box::new(Self {
|
..TaskTemplate::default()
|
||||||
tasks: task_names
|
})
|
||||||
.into_iter()
|
.collect(),
|
||||||
.enumerate()
|
);
|
||||||
.map(|(i, name)| TestTask {
|
let (tx, rx) = futures::channel::mpsc::unbounded();
|
||||||
id: TaskId(format!("task_{i}_{name}")),
|
let file = TrackedFile::new(rx, cx);
|
||||||
name,
|
tx.unbounded_send(serde_json::to_string(&tasks).unwrap())
|
||||||
})
|
.unwrap();
|
||||||
.collect(),
|
StaticSource::new(file)
|
||||||
}) as Box<dyn TaskSource>
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskSource for StaticTestSource {
|
|
||||||
fn tasks_to_schedule(
|
|
||||||
&mut self,
|
|
||||||
_cx: &mut ModelContext<Box<dyn TaskSource>>,
|
|
||||||
) -> TaskTemplates {
|
|
||||||
TaskTemplates(
|
|
||||||
self.tasks
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.map(|task| TaskTemplate {
|
|
||||||
label: task.name,
|
|
||||||
command: "test command".to_string(),
|
|
||||||
..TaskTemplate::default()
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn task_template_names(
|
pub(super) fn task_template_names(
|
||||||
|
@ -454,9 +400,9 @@ mod test_inventory {
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, _| {
|
||||||
inventory
|
inventory
|
||||||
.list_tasks(None, worktree, cx)
|
.list_tasks(None, worktree)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, task)| task.label)
|
.map(|(_, task)| task.label)
|
||||||
.sorted()
|
.sorted()
|
||||||
|
@ -469,13 +415,9 @@ mod test_inventory {
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, _| {
|
||||||
let (used, current) = inventory.used_and_current_resolved_tasks(
|
let (used, current) =
|
||||||
None,
|
inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
|
||||||
worktree,
|
|
||||||
&TaskContext::default(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
used.into_iter()
|
used.into_iter()
|
||||||
.chain(current)
|
.chain(current)
|
||||||
.map(|(_, task)| task.original_task().label.clone())
|
.map(|(_, task)| task.original_task().label.clone())
|
||||||
|
@ -488,9 +430,9 @@ mod test_inventory {
|
||||||
task_name: &str,
|
task_name: &str,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) {
|
) {
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, _| {
|
||||||
let (task_source_kind, task) = inventory
|
let (task_source_kind, task) = inventory
|
||||||
.list_tasks(None, None, cx)
|
.list_tasks(None, None)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|(_, task)| task.label == task_name)
|
.find(|(_, task)| task.label == 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}"));
|
||||||
|
@ -508,13 +450,9 @@ mod test_inventory {
|
||||||
worktree: Option<WorktreeId>,
|
worktree: Option<WorktreeId>,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Vec<(TaskSourceKind, String)> {
|
) -> Vec<(TaskSourceKind, String)> {
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, _| {
|
||||||
let (used, current) = inventory.used_and_current_resolved_tasks(
|
let (used, current) =
|
||||||
None,
|
inventory.used_and_current_resolved_tasks(None, worktree, &TaskContext::default());
|
||||||
worktree,
|
|
||||||
&TaskContext::default(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let mut all = used;
|
let mut all = used;
|
||||||
all.extend(current);
|
all.extend(current);
|
||||||
all.into_iter()
|
all.into_iter()
|
||||||
|
@ -549,27 +487,25 @@ mod tests {
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, cx| {
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
TaskSourceKind::UserInput,
|
TaskSourceKind::UserInput,
|
||||||
|cx| StaticTestSource::new(vec!["3_task".to_string()], cx),
|
static_test_source(vec!["3_task".to_string()], cx),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, cx| {
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
TaskSourceKind::UserInput,
|
TaskSourceKind::UserInput,
|
||||||
|cx| {
|
static_test_source(
|
||||||
StaticTestSource::new(
|
vec![
|
||||||
vec![
|
"1_task".to_string(),
|
||||||
"1_task".to_string(),
|
"2_task".to_string(),
|
||||||
"2_task".to_string(),
|
"1_a_task".to_string(),
|
||||||
"1_a_task".to_string(),
|
],
|
||||||
],
|
cx,
|
||||||
cx,
|
),
|
||||||
)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
let expected_initial_state = [
|
let expected_initial_state = [
|
||||||
"1_a_task".to_string(),
|
"1_a_task".to_string(),
|
||||||
"1_task".to_string(),
|
"1_task".to_string(),
|
||||||
|
@ -622,12 +558,11 @@ mod tests {
|
||||||
inventory.update(cx, |inventory, cx| {
|
inventory.update(cx, |inventory, cx| {
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
TaskSourceKind::UserInput,
|
TaskSourceKind::UserInput,
|
||||||
|cx| {
|
static_test_source(vec!["10_hello".to_string(), "11_hello".to_string()], cx),
|
||||||
StaticTestSource::new(vec!["10_hello".to_string(), "11_hello".to_string()], cx)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
let expected_updated_state = [
|
let expected_updated_state = [
|
||||||
"10_hello".to_string(),
|
"10_hello".to_string(),
|
||||||
"11_hello".to_string(),
|
"11_hello".to_string(),
|
||||||
|
@ -680,15 +615,11 @@ mod tests {
|
||||||
let worktree_path_1 = Path::new("worktree_path_1");
|
let worktree_path_1 = Path::new("worktree_path_1");
|
||||||
let worktree_2 = WorktreeId::from_usize(2);
|
let worktree_2 = WorktreeId::from_usize(2);
|
||||||
let worktree_path_2 = Path::new("worktree_path_2");
|
let worktree_path_2 = Path::new("worktree_path_2");
|
||||||
|
|
||||||
inventory_with_statics.update(cx, |inventory, cx| {
|
inventory_with_statics.update(cx, |inventory, cx| {
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
TaskSourceKind::UserInput,
|
TaskSourceKind::UserInput,
|
||||||
|cx| {
|
static_test_source(vec!["user_input".to_string(), common_name.to_string()], cx),
|
||||||
StaticTestSource::new(
|
|
||||||
vec!["user_input".to_string(), common_name.to_string()],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
|
@ -696,12 +627,10 @@ mod tests {
|
||||||
id_base: "test source",
|
id_base: "test source",
|
||||||
abs_path: path_1.to_path_buf(),
|
abs_path: path_1.to_path_buf(),
|
||||||
},
|
},
|
||||||
|cx| {
|
static_test_source(
|
||||||
StaticTestSource::new(
|
vec!["static_source_1".to_string(), common_name.to_string()],
|
||||||
vec!["static_source_1".to_string(), common_name.to_string()],
|
cx,
|
||||||
cx,
|
),
|
||||||
)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
|
@ -709,12 +638,10 @@ mod tests {
|
||||||
id_base: "test source",
|
id_base: "test source",
|
||||||
abs_path: path_2.to_path_buf(),
|
abs_path: path_2.to_path_buf(),
|
||||||
},
|
},
|
||||||
|cx| {
|
static_test_source(
|
||||||
StaticTestSource::new(
|
vec!["static_source_2".to_string(), common_name.to_string()],
|
||||||
vec!["static_source_2".to_string(), common_name.to_string()],
|
cx,
|
||||||
cx,
|
),
|
||||||
)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
|
@ -723,12 +650,7 @@ mod tests {
|
||||||
abs_path: worktree_path_1.to_path_buf(),
|
abs_path: worktree_path_1.to_path_buf(),
|
||||||
id_base: "test_source",
|
id_base: "test_source",
|
||||||
},
|
},
|
||||||
|cx| {
|
static_test_source(vec!["worktree_1".to_string(), common_name.to_string()], cx),
|
||||||
StaticTestSource::new(
|
|
||||||
vec!["worktree_1".to_string(), common_name.to_string()],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
|
@ -737,16 +659,11 @@ mod tests {
|
||||||
abs_path: worktree_path_2.to_path_buf(),
|
abs_path: worktree_path_2.to_path_buf(),
|
||||||
id_base: "test_source",
|
id_base: "test_source",
|
||||||
},
|
},
|
||||||
|cx| {
|
static_test_source(vec!["worktree_2".to_string(), common_name.to_string()], cx),
|
||||||
StaticTestSource::new(
|
|
||||||
vec!["worktree_2".to_string(), common_name.to_string()],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
let worktree_independent_tasks = vec![
|
let worktree_independent_tasks = vec![
|
||||||
(
|
(
|
||||||
TaskSourceKind::AbsPath {
|
TaskSourceKind::AbsPath {
|
||||||
|
|
|
@ -14,6 +14,7 @@ collections.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json_lenient.workspace = true
|
serde_json_lenient.workspace = true
|
||||||
|
|
|
@ -6,9 +6,8 @@ mod task_template;
|
||||||
mod vscode_format;
|
mod vscode_format;
|
||||||
|
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use gpui::ModelContext;
|
use gpui::SharedString;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::any::Any;
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
@ -103,6 +102,8 @@ pub enum VariableName {
|
||||||
Column,
|
Column,
|
||||||
/// Text from the latest selection.
|
/// Text from the latest selection.
|
||||||
SelectedText,
|
SelectedText,
|
||||||
|
/// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm
|
||||||
|
RunnableSymbol,
|
||||||
/// Custom variable, provided by the plugin or other external source.
|
/// Custom variable, provided by the plugin or other external source.
|
||||||
/// Will be printed with `ZED_` prefix to avoid potential conflicts with other variables.
|
/// Will be printed with `ZED_` prefix to avoid potential conflicts with other variables.
|
||||||
Custom(Cow<'static, str>),
|
Custom(Cow<'static, str>),
|
||||||
|
@ -132,6 +133,7 @@ impl std::fmt::Display for VariableName {
|
||||||
Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
|
Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"),
|
||||||
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
|
Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"),
|
||||||
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
|
Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"),
|
||||||
|
Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"),
|
||||||
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"),
|
Self::Custom(s) => write!(f, "{ZED_VARIABLE_NAME_PREFIX}CUSTOM_{s}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,13 +171,6 @@ pub struct TaskContext {
|
||||||
pub task_variables: TaskVariables,
|
pub task_variables: TaskVariables,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Source`] produces tasks that can be scheduled.
|
/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter.
|
||||||
///
|
#[derive(Clone, Debug)]
|
||||||
/// Implementations of this trait could be e.g. [`StaticSource`] that parses tasks from a .json files and provides process templates to be spawned;
|
pub struct RunnableTag(pub SharedString);
|
||||||
/// another one could be a language server providing lenses with tests or build server listing all targets for a given project.
|
|
||||||
pub trait TaskSource: Any {
|
|
||||||
/// A way to erase the type of the source, processing and storing them generically.
|
|
||||||
fn as_any(&mut self) -> &mut dyn Any;
|
|
||||||
/// Collects all tasks available for scheduling.
|
|
||||||
fn tasks_to_schedule(&mut self, cx: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,134 +1,110 @@
|
||||||
//! 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::sync::Arc;
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
use gpui::AppContext;
|
||||||
|
use parking_lot::RwLock;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
use crate::{TaskSource, TaskTemplates};
|
use crate::TaskTemplates;
|
||||||
use futures::channel::mpsc::UnboundedReceiver;
|
use futures::channel::mpsc::UnboundedReceiver;
|
||||||
|
|
||||||
/// The source of tasks defined in a tasks config file.
|
/// The source of tasks defined in a tasks config file.
|
||||||
pub struct StaticSource {
|
pub struct StaticSource {
|
||||||
tasks: TaskTemplates,
|
tasks: TrackedFile<TaskTemplates>,
|
||||||
_templates: Model<TrackedFile<TaskTemplates>>,
|
|
||||||
_subscription: Subscription,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A Wrapper around deserializable T that keeps track of its contents
|
/// A Wrapper around deserializable T that keeps track of its contents
|
||||||
/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
|
/// via a provided channel. Once T value changes, the observers of [`TrackedFile`] are
|
||||||
/// notified.
|
/// notified.
|
||||||
pub struct TrackedFile<T> {
|
pub struct TrackedFile<T> {
|
||||||
parsed_contents: T,
|
parsed_contents: Arc<RwLock<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialEq + 'static> TrackedFile<T> {
|
impl<T: PartialEq + 'static + Sync> TrackedFile<T> {
|
||||||
/// Initializes new [`TrackedFile`] with a type that's deserializable.
|
/// Initializes new [`TrackedFile`] with a type that's deserializable.
|
||||||
pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Model<Self>
|
pub fn new(mut tracker: UnboundedReceiver<String>, cx: &mut AppContext) -> Self
|
||||||
where
|
where
|
||||||
T: for<'a> Deserialize<'a> + Default,
|
T: for<'a> Deserialize<'a> + Default + Send,
|
||||||
{
|
{
|
||||||
cx.new_model(move |cx| {
|
let parsed_contents: Arc<RwLock<T>> = Arc::default();
|
||||||
cx.spawn(|tracked_file, mut cx| async move {
|
cx.background_executor()
|
||||||
while let Some(new_contents) = tracker.next().await {
|
.spawn({
|
||||||
if !new_contents.trim().is_empty() {
|
let parsed_contents = parsed_contents.clone();
|
||||||
// String -> T (ZedTaskFormat)
|
async move {
|
||||||
// String -> U (VsCodeFormat) -> Into::into T
|
while let Some(new_contents) = tracker.next().await {
|
||||||
let Some(new_contents) =
|
if Arc::strong_count(&parsed_contents) == 1 {
|
||||||
serde_json_lenient::from_str(&new_contents).log_err()
|
// We're no longer being observed. Stop polling.
|
||||||
else {
|
break;
|
||||||
continue;
|
}
|
||||||
};
|
if !new_contents.trim().is_empty() {
|
||||||
tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
|
let Some(new_contents) =
|
||||||
if tracked_file.parsed_contents != new_contents {
|
serde_json_lenient::from_str::<T>(&new_contents).log_err()
|
||||||
tracked_file.parsed_contents = new_contents;
|
else {
|
||||||
cx.notify();
|
continue;
|
||||||
};
|
};
|
||||||
})?;
|
let mut contents = parsed_contents.write();
|
||||||
|
*contents = new_contents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
Self {
|
Self { parsed_contents }
|
||||||
parsed_contents: Default::default(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type.
|
/// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type.
|
||||||
pub fn new_convertible<U: for<'a> Deserialize<'a> + TryInto<T, Error = anyhow::Error>>(
|
pub fn new_convertible<U: for<'a> Deserialize<'a> + TryInto<T, Error = anyhow::Error>>(
|
||||||
mut tracker: UnboundedReceiver<String>,
|
mut tracker: UnboundedReceiver<String>,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Model<Self>
|
) -> Self
|
||||||
where
|
where
|
||||||
T: Default,
|
T: Default + Send,
|
||||||
{
|
{
|
||||||
cx.new_model(move |cx| {
|
let parsed_contents: Arc<RwLock<T>> = Arc::default();
|
||||||
cx.spawn(|tracked_file, mut cx| async move {
|
cx.background_executor()
|
||||||
while let Some(new_contents) = tracker.next().await {
|
.spawn({
|
||||||
if !new_contents.trim().is_empty() {
|
let parsed_contents = parsed_contents.clone();
|
||||||
let Some(new_contents) =
|
async move {
|
||||||
serde_json_lenient::from_str::<U>(&new_contents).log_err()
|
while let Some(new_contents) = tracker.next().await {
|
||||||
else {
|
if Arc::strong_count(&parsed_contents) == 1 {
|
||||||
continue;
|
// We're no longer being observed. Stop polling.
|
||||||
};
|
break;
|
||||||
let Some(new_contents) = new_contents.try_into().log_err() else {
|
}
|
||||||
continue;
|
|
||||||
};
|
if !new_contents.trim().is_empty() {
|
||||||
tracked_file.update(&mut cx, |tracked_file: &mut TrackedFile<T>, cx| {
|
let Some(new_contents) =
|
||||||
if tracked_file.parsed_contents != new_contents {
|
serde_json_lenient::from_str::<U>(&new_contents).log_err()
|
||||||
tracked_file.parsed_contents = new_contents;
|
else {
|
||||||
cx.notify();
|
continue;
|
||||||
};
|
};
|
||||||
})?;
|
let Some(new_contents) = new_contents.try_into().log_err() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut contents = parsed_contents.write();
|
||||||
|
*contents = new_contents;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
Self {
|
Self {
|
||||||
parsed_contents: Default::default(),
|
parsed_contents: Default::default(),
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self) -> &T {
|
|
||||||
&self.parsed_contents
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(tasks: TrackedFile<TaskTemplates>) -> Self {
|
||||||
templates: Model<TrackedFile<TaskTemplates>>,
|
Self { tasks }
|
||||||
cx: &mut AppContext,
|
}
|
||||||
) -> Model<Box<dyn TaskSource>> {
|
/// Returns current list of tasks
|
||||||
cx.new_model(|cx| {
|
pub fn tasks_to_schedule(&self) -> TaskTemplates {
|
||||||
let _subscription = cx.observe(
|
self.tasks.parsed_contents.read().clone()
|
||||||
&templates,
|
|
||||||
move |source: &mut Box<(dyn TaskSource + 'static)>, new_templates, cx| {
|
|
||||||
if let Some(static_source) = source.as_any().downcast_mut::<Self>() {
|
|
||||||
static_source.tasks = new_templates.read(cx).get().clone();
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
Box::new(Self {
|
|
||||||
tasks: TaskTemplates::default(),
|
|
||||||
_templates: templates,
|
|
||||||
_subscription,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskSource for StaticSource {
|
|
||||||
fn tasks_to_schedule(&mut self, _: &mut ModelContext<Box<dyn TaskSource>>) -> TaskTemplates {
|
|
||||||
self.tasks.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,10 @@ pub struct TaskTemplate {
|
||||||
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub reveal: RevealStrategy,
|
pub reveal: RevealStrategy,
|
||||||
|
|
||||||
|
/// Represents the tags which this template attaches to. Adding this removes this task from other UI.
|
||||||
|
#[serde(default)]
|
||||||
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What to do with the terminal pane and tab, after the command was started.
|
/// What to do with the terminal pane and tab, after the command was started.
|
||||||
|
|
|
@ -9,7 +9,6 @@ license = "GPL-3.0-or-later"
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
file_icons.workspace = true
|
file_icons.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
use std::{
|
use std::sync::Arc;
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ::settings::Settings;
|
use ::settings::Settings;
|
||||||
use anyhow::Context;
|
use editor::{tasks::task_context, Editor};
|
||||||
use editor::Editor;
|
|
||||||
use gpui::{AppContext, ViewContext, WindowContext};
|
use gpui::{AppContext, ViewContext, WindowContext};
|
||||||
use language::{BasicContextProvider, ContextProvider, Language};
|
use language::Language;
|
||||||
use modal::TasksModal;
|
use modal::TasksModal;
|
||||||
use project::{Location, TaskSourceKind, WorktreeId};
|
use project::WorktreeId;
|
||||||
use task::{ResolvedTask, TaskContext, TaskTemplate, TaskVariables};
|
use workspace::tasks::schedule_task;
|
||||||
use util::ResultExt;
|
use workspace::{tasks::schedule_resolved_task, Workspace};
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
mod modal;
|
mod modal;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
@ -97,9 +92,9 @@ fn spawn_task_with_name(name: String, cx: &mut ViewContext<Workspace>) {
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
let (worktree, language) = active_item_selection_properties(workspace, cx);
|
let (worktree, language) = active_item_selection_properties(workspace, cx);
|
||||||
let tasks = workspace.project().update(cx, |project, cx| {
|
let tasks = workspace.project().update(cx, |project, cx| {
|
||||||
project.task_inventory().update(cx, |inventory, cx| {
|
project
|
||||||
inventory.list_tasks(language, worktree, cx)
|
.task_inventory()
|
||||||
})
|
.update(cx, |inventory, _| inventory.list_tasks(language, worktree))
|
||||||
});
|
});
|
||||||
let (task_source_kind, target_task) =
|
let (task_source_kind, target_task) =
|
||||||
tasks.into_iter().find(|(_, task)| task.label == name)?;
|
tasks.into_iter().find(|(_, task)| task.label == name)?;
|
||||||
|
@ -152,168 +147,6 @@ fn active_item_selection_properties(
|
||||||
(worktree_id, language)
|
(worktree_id, language)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn task_context(workspace: &Workspace, cx: &mut WindowContext<'_>) -> TaskContext {
|
|
||||||
fn task_context_impl(workspace: &Workspace, cx: &mut WindowContext<'_>) -> Option<TaskContext> {
|
|
||||||
let cwd = task_cwd(workspace, cx).log_err().flatten();
|
|
||||||
let editor = workspace
|
|
||||||
.active_item(cx)
|
|
||||||
.and_then(|item| item.act_as::<Editor>(cx))?;
|
|
||||||
|
|
||||||
let (selection, buffer, editor_snapshot) = editor.update(cx, |editor, cx| {
|
|
||||||
let selection = editor.selections.newest::<usize>(cx);
|
|
||||||
let (buffer, _, _) = editor
|
|
||||||
.buffer()
|
|
||||||
.read(cx)
|
|
||||||
.point_to_buffer_offset(selection.start, cx)?;
|
|
||||||
let snapshot = editor.snapshot(cx);
|
|
||||||
Some((selection, buffer, snapshot))
|
|
||||||
})?;
|
|
||||||
let language_context_provider = buffer
|
|
||||||
.read(cx)
|
|
||||||
.language()
|
|
||||||
.and_then(|language| language.context_provider())
|
|
||||||
.unwrap_or_else(|| Arc::new(BasicContextProvider));
|
|
||||||
let selection_range = selection.range();
|
|
||||||
let start = editor_snapshot
|
|
||||||
.display_snapshot
|
|
||||||
.buffer_snapshot
|
|
||||||
.anchor_after(selection_range.start)
|
|
||||||
.text_anchor;
|
|
||||||
let end = editor_snapshot
|
|
||||||
.display_snapshot
|
|
||||||
.buffer_snapshot
|
|
||||||
.anchor_after(selection_range.end)
|
|
||||||
.text_anchor;
|
|
||||||
let worktree_abs_path = buffer
|
|
||||||
.read(cx)
|
|
||||||
.file()
|
|
||||||
.map(|file| WorktreeId::from_usize(file.worktree_id()))
|
|
||||||
.and_then(|worktree_id| {
|
|
||||||
workspace
|
|
||||||
.project()
|
|
||||||
.read(cx)
|
|
||||||
.worktree_for_id(worktree_id, cx)
|
|
||||||
.map(|worktree| worktree.read(cx).abs_path())
|
|
||||||
});
|
|
||||||
let location = Location {
|
|
||||||
buffer,
|
|
||||||
range: start..end,
|
|
||||||
};
|
|
||||||
let task_variables = combine_task_variables(
|
|
||||||
worktree_abs_path.as_deref(),
|
|
||||||
location,
|
|
||||||
language_context_provider.as_ref(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.log_err()?;
|
|
||||||
Some(TaskContext {
|
|
||||||
cwd,
|
|
||||||
task_variables,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
task_context_impl(workspace, cx).unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn combine_task_variables(
|
|
||||||
worktree_abs_path: Option<&Path>,
|
|
||||||
location: Location,
|
|
||||||
context_provider: &dyn ContextProvider,
|
|
||||||
cx: &mut WindowContext<'_>,
|
|
||||||
) -> anyhow::Result<TaskVariables> {
|
|
||||||
if context_provider.is_basic() {
|
|
||||||
context_provider
|
|
||||||
.build_context(worktree_abs_path, &location, cx)
|
|
||||||
.context("building basic provider context")
|
|
||||||
} else {
|
|
||||||
let mut basic_context = BasicContextProvider
|
|
||||||
.build_context(worktree_abs_path, &location, cx)
|
|
||||||
.context("building basic default context")?;
|
|
||||||
basic_context.extend(
|
|
||||||
context_provider
|
|
||||||
.build_context(worktree_abs_path, &location, cx)
|
|
||||||
.context("building provider context ")?,
|
|
||||||
);
|
|
||||||
Ok(basic_context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schedule_task(
|
|
||||||
workspace: &Workspace,
|
|
||||||
task_source_kind: TaskSourceKind,
|
|
||||||
task_to_resolve: &TaskTemplate,
|
|
||||||
task_cx: &TaskContext,
|
|
||||||
omit_history: bool,
|
|
||||||
cx: &mut ViewContext<'_, Workspace>,
|
|
||||||
) {
|
|
||||||
if let Some(spawn_in_terminal) =
|
|
||||||
task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
|
|
||||||
{
|
|
||||||
schedule_resolved_task(
|
|
||||||
workspace,
|
|
||||||
task_source_kind,
|
|
||||||
spawn_in_terminal,
|
|
||||||
omit_history,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schedule_resolved_task(
|
|
||||||
workspace: &Workspace,
|
|
||||||
task_source_kind: TaskSourceKind,
|
|
||||||
mut resolved_task: ResolvedTask,
|
|
||||||
omit_history: bool,
|
|
||||||
cx: &mut ViewContext<'_, Workspace>,
|
|
||||||
) {
|
|
||||||
if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
|
|
||||||
if !omit_history {
|
|
||||||
resolved_task.resolved = Some(spawn_in_terminal.clone());
|
|
||||||
workspace.project().update(cx, |project, cx| {
|
|
||||||
project.task_inventory().update(cx, |inventory, _| {
|
|
||||||
inventory.task_scheduled(task_source_kind, resolved_task);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
cx.emit(workspace::Event::SpawnTask(spawn_in_terminal));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
|
|
||||||
let project = workspace.project().read(cx);
|
|
||||||
let available_worktrees = project
|
|
||||||
.worktrees()
|
|
||||||
.filter(|worktree| {
|
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
worktree.is_visible()
|
|
||||||
&& worktree.is_local()
|
|
||||||
&& worktree.root_entry().map_or(false, |e| e.is_dir())
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let cwd = match available_worktrees.len() {
|
|
||||||
0 => None,
|
|
||||||
1 => Some(available_worktrees[0].read(cx).abs_path()),
|
|
||||||
_ => {
|
|
||||||
let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
|
|
||||||
available_worktrees.into_iter().find_map(|worktree| {
|
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
if worktree.contains_entry(entry_id) {
|
|
||||||
Some(worktree.abs_path())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
anyhow::ensure!(
|
|
||||||
cwd_for_active_entry.is_some(),
|
|
||||||
"Cannot determine task cwd for multiple worktrees"
|
|
||||||
);
|
|
||||||
cwd_for_active_entry
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(cwd.map(|path| path.to_path_buf()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{active_item_selection_properties, schedule_resolved_task};
|
use crate::active_item_selection_properties;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement,
|
impl_actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement,
|
||||||
|
@ -16,7 +16,7 @@ use ui::{
|
||||||
Tooltip, WindowContext,
|
Tooltip, WindowContext,
|
||||||
};
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{ModalView, Workspace};
|
use workspace::{tasks::schedule_resolved_task, ModalView, Workspace};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
@ -211,12 +211,11 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let (used, current) =
|
let (used, current) =
|
||||||
picker.delegate.inventory.update(cx, |inventory, cx| {
|
picker.delegate.inventory.update(cx, |inventory, _| {
|
||||||
inventory.used_and_current_resolved_tasks(
|
inventory.used_and_current_resolved_tasks(
|
||||||
language,
|
language,
|
||||||
worktree,
|
worktree,
|
||||||
&picker.delegate.task_context,
|
&picker.delegate.task_context,
|
||||||
cx,
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
picker.delegate.last_used_candidate_index = if used.is_empty() {
|
picker.delegate.last_used_candidate_index = if used.is_empty() {
|
||||||
|
|
83
crates/workspace/src/tasks.rs
Normal file
83
crates/workspace/src/tasks.rs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use project::TaskSourceKind;
|
||||||
|
use task::{ResolvedTask, TaskContext, TaskTemplate};
|
||||||
|
use ui::{ViewContext, WindowContext};
|
||||||
|
|
||||||
|
use crate::Workspace;
|
||||||
|
|
||||||
|
pub fn task_cwd(workspace: &Workspace, cx: &mut WindowContext) -> anyhow::Result<Option<PathBuf>> {
|
||||||
|
let project = workspace.project().read(cx);
|
||||||
|
let available_worktrees = project
|
||||||
|
.worktrees()
|
||||||
|
.filter(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
worktree.is_visible()
|
||||||
|
&& worktree.is_local()
|
||||||
|
&& worktree.root_entry().map_or(false, |e| e.is_dir())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let cwd = match available_worktrees.len() {
|
||||||
|
0 => None,
|
||||||
|
1 => Some(available_worktrees[0].read(cx).abs_path()),
|
||||||
|
_ => {
|
||||||
|
let cwd_for_active_entry = project.active_entry().and_then(|entry_id| {
|
||||||
|
available_worktrees.into_iter().find_map(|worktree| {
|
||||||
|
let worktree = worktree.read(cx);
|
||||||
|
if worktree.contains_entry(entry_id) {
|
||||||
|
Some(worktree.abs_path())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
anyhow::ensure!(
|
||||||
|
cwd_for_active_entry.is_some(),
|
||||||
|
"Cannot determine task cwd for multiple worktrees"
|
||||||
|
);
|
||||||
|
cwd_for_active_entry
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(cwd.map(|path| path.to_path_buf()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schedule_task(
|
||||||
|
workspace: &Workspace,
|
||||||
|
task_source_kind: TaskSourceKind,
|
||||||
|
task_to_resolve: &TaskTemplate,
|
||||||
|
task_cx: &TaskContext,
|
||||||
|
omit_history: bool,
|
||||||
|
cx: &mut ViewContext<'_, Workspace>,
|
||||||
|
) {
|
||||||
|
if let Some(spawn_in_terminal) =
|
||||||
|
task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx)
|
||||||
|
{
|
||||||
|
schedule_resolved_task(
|
||||||
|
workspace,
|
||||||
|
task_source_kind,
|
||||||
|
spawn_in_terminal,
|
||||||
|
omit_history,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schedule_resolved_task(
|
||||||
|
workspace: &Workspace,
|
||||||
|
task_source_kind: TaskSourceKind,
|
||||||
|
mut resolved_task: ResolvedTask,
|
||||||
|
omit_history: bool,
|
||||||
|
cx: &mut ViewContext<'_, Workspace>,
|
||||||
|
) {
|
||||||
|
if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
|
||||||
|
if !omit_history {
|
||||||
|
resolved_task.resolved = Some(spawn_in_terminal.clone());
|
||||||
|
workspace.project().update(cx, |project, cx| {
|
||||||
|
project.task_inventory().update(cx, |inventory, _| {
|
||||||
|
inventory.task_scheduled(task_source_kind, resolved_task);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cx.emit(crate::Event::SpawnTask(spawn_in_terminal));
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ mod persistence;
|
||||||
pub mod searchable;
|
pub mod searchable;
|
||||||
pub mod shared_screen;
|
pub mod shared_screen;
|
||||||
mod status_bar;
|
mod status_bar;
|
||||||
|
pub mod tasks;
|
||||||
mod toolbar;
|
mod toolbar;
|
||||||
mod workspace_settings;
|
mod workspace_settings;
|
||||||
|
|
||||||
|
|
|
@ -297,7 +297,7 @@ fn init_ui(args: Args) {
|
||||||
|
|
||||||
load_user_themes_in_background(fs.clone(), cx);
|
load_user_themes_in_background(fs.clone(), cx);
|
||||||
watch_themes(fs.clone(), cx);
|
watch_themes(fs.clone(), cx);
|
||||||
|
watch_languages(fs.clone(), languages.clone(), cx);
|
||||||
watch_file_types(fs.clone(), cx);
|
watch_file_types(fs.clone(), cx);
|
||||||
|
|
||||||
languages.set_theme(cx.theme().clone());
|
languages.set_theme(cx.theme().clone());
|
||||||
|
@ -861,6 +861,37 @@ fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
|
||||||
.detach()
|
.detach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>, cx: &mut AppContext) {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
let path = {
|
||||||
|
let p = Path::new("crates/languages/src");
|
||||||
|
let Ok(full_path) = p.canonicalize() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
full_path
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.spawn(|_| async move {
|
||||||
|
let mut events = fs.watch(path.as_path(), Duration::from_millis(100)).await;
|
||||||
|
while let Some(event) = events.next().await {
|
||||||
|
let has_language_file = event.iter().any(|path| {
|
||||||
|
path.extension()
|
||||||
|
.map(|ext| ext.to_string_lossy().as_ref() == "scm")
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
if has_language_file {
|
||||||
|
languages.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
fn watch_languages(_fs: Arc<dyn fs::Fs>, _languages: Arc<LanguageRegistry>, _cx: &mut AppContext) {}
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
|
fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
|
@ -168,19 +168,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
let fs = app_state.fs.clone();
|
let fs = app_state.fs.clone();
|
||||||
project.task_inventory().update(cx, |inventory, cx| {
|
project.task_inventory().update(cx, |inventory, cx| {
|
||||||
|
let tasks_file_rx =
|
||||||
|
watch_config_file(&cx.background_executor(), fs, paths::TASKS.clone());
|
||||||
inventory.add_source(
|
inventory.add_source(
|
||||||
TaskSourceKind::AbsPath {
|
TaskSourceKind::AbsPath {
|
||||||
id_base: "global_tasks",
|
id_base: "global_tasks",
|
||||||
abs_path: paths::TASKS.clone(),
|
abs_path: paths::TASKS.clone(),
|
||||||
},
|
},
|
||||||
|cx| {
|
StaticSource::new(TrackedFile::new(tasks_file_rx, cx)),
|
||||||
let tasks_file_rx = watch_config_file(
|
|
||||||
&cx.background_executor(),
|
|
||||||
fs,
|
|
||||||
paths::TASKS.clone(),
|
|
||||||
);
|
|
||||||
StaticSource::new(TrackedFile::new(tasks_file_rx, cx), cx)
|
|
||||||
},
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue