Detailed changes
@@ -16,6 +16,7 @@
"allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
+ // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
"reveal": "always",
// What to do with the terminal pane and tab, after the command had finished:
@@ -159,6 +159,13 @@ pub struct DeleteToPreviousWordStart {
pub struct FoldAtLevel {
pub level: u32,
}
+
+#[derive(PartialEq, Clone, Deserialize, Default)]
+pub struct SpawnNearestTask {
+ #[serde(default)]
+ pub reveal: task::RevealStrategy,
+}
+
impl_actions!(
editor,
[
@@ -184,6 +191,7 @@ impl_actions!(
SelectToBeginningOfLine,
SelectToEndOfLine,
SelectUpByLines,
+ SpawnNearestTask,
ShowCompletions,
ToggleCodeActions,
ToggleComments,
@@ -502,6 +502,19 @@ struct RunnableTasks {
context_range: Range<BufferOffset>,
}
+impl RunnableTasks {
+ fn resolve<'a>(
+ &'a self,
+ cx: &'a task::TaskContext,
+ ) -> impl Iterator<Item = (TaskSourceKind, ResolvedTask)> + 'a {
+ self.templates.iter().filter_map(|(kind, template)| {
+ template
+ .resolve_task(&kind.to_id_base(), cx)
+ .map(|task| (kind.clone(), task))
+ })
+ }
+}
+
#[derive(Clone)]
struct ResolvedTasks {
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
@@ -4723,29 +4736,7 @@ impl Editor {
.as_ref()
.zip(editor.project.clone())
.map(|(tasks, project)| {
- let position = Point::new(buffer_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,
- };
- // Fill in the environmental variables from the tree-sitter captures
- let mut captured_task_variables = TaskVariables::default();
- for (capture_name, value) in tasks.extra_variables.clone() {
- captured_task_variables.insert(
- task::VariableName::Custom(capture_name.into()),
- value.clone(),
- );
- }
- project.update(cx, |project, cx| {
- project.task_store().update(cx, |task_store, cx| {
- task_store.task_context_for_location(
- captured_task_variables,
- location,
- cx,
- )
- })
- })
+ Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
});
Some(cx.spawn(|editor, mut cx| async move {
@@ -4756,15 +4747,7 @@ impl Editor {
let resolved_tasks =
tasks.zip(task_context).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(),
+ templates: tasks.resolve(&task_context).collect(),
position: snapshot.buffer_snapshot.anchor_before(Point::new(
multibuffer_point.row,
tasks.column,
@@ -5470,6 +5453,132 @@ impl Editor {
}
}
+ fn build_tasks_context(
+ project: &Model<Project>,
+ buffer: &Model<Buffer>,
+ buffer_row: u32,
+ tasks: &Arc<RunnableTasks>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Option<task::TaskContext>> {
+ let position = Point::new(buffer_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,
+ };
+ // Fill in the environmental variables from the tree-sitter captures
+ let mut captured_task_variables = TaskVariables::default();
+ for (capture_name, value) in tasks.extra_variables.clone() {
+ captured_task_variables.insert(
+ task::VariableName::Custom(capture_name.into()),
+ value.clone(),
+ );
+ }
+ project.update(cx, |project, cx| {
+ project.task_store().update(cx, |task_store, cx| {
+ task_store.task_context_for_location(captured_task_variables, location, cx)
+ })
+ })
+ }
+
+ pub fn spawn_nearest_task(&mut self, action: &SpawnNearestTask, cx: &mut ViewContext<Self>) {
+ let Some((workspace, _)) = self.workspace.clone() else {
+ return;
+ };
+ let Some(project) = self.project.clone() else {
+ return;
+ };
+
+ // Try to find a closest, enclosing node using tree-sitter that has a
+ // task
+ let Some((buffer, buffer_row, tasks)) = self
+ .find_enclosing_node_task(cx)
+ // Or find the task that's closest in row-distance.
+ .or_else(|| self.find_closest_task(cx))
+ else {
+ return;
+ };
+
+ let reveal_strategy = action.reveal;
+ let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
+ cx.spawn(|_, mut cx| async move {
+ let context = task_context.await?;
+ let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
+
+ let resolved = resolved_task.resolved.as_mut()?;
+ resolved.reveal = reveal_strategy;
+
+ workspace
+ .update(&mut cx, |workspace, cx| {
+ workspace::tasks::schedule_resolved_task(
+ workspace,
+ task_source_kind,
+ resolved_task,
+ false,
+ cx,
+ );
+ })
+ .ok()
+ })
+ .detach();
+ }
+
+ fn find_closest_task(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
+ let cursor_row = self.selections.newest_adjusted(cx).head().row;
+
+ let ((buffer_id, row), tasks) = self
+ .tasks
+ .iter()
+ .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?;
+
+ let buffer = self.buffer.read(cx).buffer(*buffer_id)?;
+ let tasks = Arc::new(tasks.to_owned());
+ Some((buffer, *row, tasks))
+ }
+
+ fn find_enclosing_node_task(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<(Model<Buffer>, u32, Arc<RunnableTasks>)> {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let offset = self.selections.newest::<usize>(cx).head();
+ let excerpt = snapshot.excerpt_containing(offset..offset)?;
+ let buffer_id = excerpt.buffer().remote_id();
+
+ let layer = excerpt.buffer().syntax_layer_at(offset)?;
+ let mut cursor = layer.node().walk();
+
+ while cursor.goto_first_child_for_byte(offset).is_some() {
+ if cursor.node().end_byte() == offset {
+ cursor.goto_next_sibling();
+ }
+ }
+
+ // Ascend to the smallest ancestor that contains the range and has a task.
+ loop {
+ let node = cursor.node();
+ let node_range = node.byte_range();
+ let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row;
+
+ // Check if this node contains our offset
+ if node_range.start <= offset && node_range.end >= offset {
+ // If it contains offset, check for task
+ if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) {
+ let buffer = self.buffer.read(cx).buffer(buffer_id)?;
+ return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned())));
+ }
+ }
+
+ if !cursor.goto_parent() {
+ break;
+ }
+ }
+ None
+ }
+
fn render_run_indicator(
&self,
_style: &EditorStyle,
@@ -13330,6 +13330,89 @@ async fn test_goto_definition_with_find_all_references_fallback(cx: &mut gpui::T
});
}
+#[gpui::test]
+async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let language = Arc::new(Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ ));
+
+ let text = r#"
+ #[cfg(test)]
+ mod tests() {
+ #[test]
+ fn runnable_1() {
+ let a = 1;
+ }
+
+ #[test]
+ fn runnable_2() {
+ let a = 1;
+ let b = 2;
+ }
+ }
+ "#
+ .unindent();
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_file("/file.rs", Default::default()).await;
+
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
+ let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
+ let multi_buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
+
+ let editor = cx.new_view(|cx| {
+ Editor::new(
+ EditorMode::Full,
+ multi_buffer,
+ Some(project.clone()),
+ true,
+ cx,
+ )
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.tasks.insert(
+ (buffer.read(cx).remote_id(), 3),
+ RunnableTasks {
+ templates: vec![],
+ offset: MultiBufferOffset(43),
+ column: 0,
+ extra_variables: HashMap::default(),
+ context_range: BufferOffset(43)..BufferOffset(85),
+ },
+ );
+ editor.tasks.insert(
+ (buffer.read(cx).remote_id(), 8),
+ RunnableTasks {
+ templates: vec![],
+ offset: MultiBufferOffset(86),
+ column: 0,
+ extra_variables: HashMap::default(),
+ context_range: BufferOffset(86)..BufferOffset(191),
+ },
+ );
+
+ // Test finding task when cursor is inside function body
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
+ });
+ let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
+ assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
+
+ // Test finding task when cursor is on function name
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
+ });
+ let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
+ assert_eq!(row, 8, "Should find task when cursor is on function name");
+ });
+}
+
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point
@@ -449,7 +449,8 @@ impl EditorElement {
register_action(view, cx, Editor::apply_all_diff_hunks);
register_action(view, cx, Editor::apply_selected_diff_hunks);
register_action(view, cx, Editor::open_active_item_in_terminal);
- register_action(view, cx, Editor::reload_file)
+ register_action(view, cx, Editor::reload_file);
+ register_action(view, cx, Editor::spawn_nearest_task);
}
fn register_key_listeners(&self, cx: &mut WindowContext, layout: &EditorLayout) {
@@ -66,6 +66,8 @@ pub enum RevealStrategy {
/// Always show the terminal pane, add and focus the corresponding task's tab in it.
#[default]
Always,
+ /// Always show the terminal pane, add the task's tab in it, but don't focus it.
+ NoFocus,
/// Do not change terminal pane focus, but still add/reuse the task's tab there.
Never,
}
@@ -575,9 +575,9 @@ impl TerminalPanel {
.collect()
}
- fn activate_terminal_view(&self, item_index: usize, cx: &mut WindowContext) {
+ fn activate_terminal_view(&self, item_index: usize, focus: bool, cx: &mut WindowContext) {
self.pane.update(cx, |pane, cx| {
- pane.activate_item(item_index, true, true, cx)
+ pane.activate_item(item_index, true, focus, cx)
})
}
@@ -616,8 +616,14 @@ impl TerminalPanel {
pane.add_item(terminal_view, true, focus, None, cx);
});
- if reveal_strategy == RevealStrategy::Always {
- workspace.focus_panel::<Self>(cx);
+ match reveal_strategy {
+ RevealStrategy::Always => {
+ workspace.focus_panel::<Self>(cx);
+ }
+ RevealStrategy::NoFocus => {
+ workspace.open_panel::<Self>(cx);
+ }
+ RevealStrategy::Never => {}
}
Ok(terminal)
})?;
@@ -698,7 +704,7 @@ impl TerminalPanel {
match reveal {
RevealStrategy::Always => {
- self.activate_terminal_view(terminal_item_index, cx);
+ self.activate_terminal_view(terminal_item_index, true, cx);
let task_workspace = self.workspace.clone();
cx.spawn(|_, mut cx| async move {
task_workspace
@@ -707,6 +713,16 @@ impl TerminalPanel {
})
.detach();
}
+ RevealStrategy::NoFocus => {
+ self.activate_terminal_view(terminal_item_index, false, cx);
+ let task_workspace = self.workspace.clone();
+ cx.spawn(|_, mut cx| async move {
+ task_workspace
+ .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
+ .ok()
+ })
+ .detach();
+ }
RevealStrategy::Never => {}
}
@@ -18,6 +18,7 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
"allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
+ // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
"reveal": "always",
// What to do with the terminal pane and tab, after the command had finished: