From db48c75231a73ae0ad07667429fa88a460c966d8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 15 Apr 2024 15:07:21 +0200 Subject: [PATCH] Add basic bash and Python tasks (#10548) Part of https://github.com/zed-industries/zed/issues/5141 * adds "run selection" and "run file" tasks for bash and Python. * replaces newlines with `\n` symbols in the human-readable task labels * properly escapes task command arguments when spawning the task in terminal Caveats: * bash tasks will always use user's default shell to spawn the selections, but they should rather respect the shebang line even if it's not selected * Python tasks will always use `python3` to spawn its tasks now, as there's no proper mechanism in Zed to deal with different Python executables Release Notes: - Added tasks for bash and Python to execute selections and open files in terminal --- Cargo.lock | 1 + Cargo.toml | 1 + crates/languages/src/bash.rs | 18 +++++++++++++ crates/languages/src/lib.rs | 11 +++++--- crates/languages/src/python.rs | 27 ++++++++++++++++++- crates/project/src/task_inventory.rs | 23 ++++++++++------- crates/project/src/terminals.rs | 1 + crates/task/src/task_template.rs | 30 +++++++++++++++++++--- crates/terminal/src/terminal.rs | 1 + crates/terminal_view/Cargo.toml | 1 + crates/terminal_view/src/terminal_panel.rs | 3 ++- 11 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 crates/languages/src/bash.rs diff --git a/Cargo.lock b/Cargo.lock index f90f4d3a05..00cda1909b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9842,6 +9842,7 @@ dependencies = [ "serde_json", "settings", "shellexpand", + "shlex", "smol", "task", "terminal", diff --git a/Cargo.toml b/Cargo.toml index 6983f0d99f..b763ed706e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -298,6 +298,7 @@ serde_json_lenient = { version = "0.1", features = [ ] } serde_repr = "0.1" sha2 = "0.10" +shlex = "1.3" shellexpand = "2.1.0" smallvec = { version = "1.6", features = ["union"] } smol = "1.2" diff --git a/crates/languages/src/bash.rs b/crates/languages/src/bash.rs new file mode 100644 index 0000000000..5cdcf86a58 --- /dev/null +++ b/crates/languages/src/bash.rs @@ -0,0 +1,18 @@ +use language::ContextProviderWithTasks; +use task::{TaskTemplate, TaskTemplates, VariableName}; + +pub(super) fn bash_task_context() -> ContextProviderWithTasks { + ContextProviderWithTasks::new(TaskTemplates(vec![ + TaskTemplate { + label: "execute selection".to_owned(), + command: VariableName::SelectedText.template_value(), + ignore_previously_resolved: true, + ..TaskTemplate::default() + }, + TaskTemplate { + label: format!("run '{}'", VariableName::File.template_value()), + command: VariableName::File.template_value(), + ..TaskTemplate::default() + }, + ])) +} diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index cc16d26870..6262ee1c0e 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -8,10 +8,14 @@ use smol::stream::StreamExt; use std::{str, sync::Arc}; use util::{asset_str, ResultExt}; -use crate::{elixir::elixir_task_context, rust::RustContextProvider}; +use crate::{ + bash::bash_task_context, elixir::elixir_task_context, python::python_task_context, + rust::RustContextProvider, +}; use self::{deno::DenoSettings, elixir::ElixirSettings}; +mod bash; mod c; mod css; mod deno; @@ -133,7 +137,7 @@ pub fn init( ); }; } - language!("bash"); + language!("bash", Vec::new(), bash_task_context()); language!("c", vec![Arc::new(c::CLspAdapter) as Arc]); language!("cpp", vec![Arc::new(c::CLspAdapter)]); language!( @@ -195,7 +199,8 @@ pub fn init( "python", vec![Arc::new(python::PythonLspAdapter::new( node_runtime.clone(), - ))] + ))], + python_task_context() ); language!( "rust", diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 560385082e..192cb1b114 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -1,6 +1,6 @@ use anyhow::Result; use async_trait::async_trait; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{ContextProviderWithTasks, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use std::{ @@ -9,6 +9,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use task::{TaskTemplate, TaskTemplates, VariableName}; use util::ResultExt; const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js"; @@ -180,6 +181,30 @@ async fn get_cached_server_binary( } } +pub(super) fn python_task_context() -> ContextProviderWithTasks { + ContextProviderWithTasks::new(TaskTemplates(vec![ + TaskTemplate { + label: "execute selection".to_owned(), + command: "python3".to_owned(), + args: vec![ + "-c".to_owned(), + format!( + "exec(r'''{}''')", + VariableName::SelectedText.template_value() + ), + ], + ignore_previously_resolved: true, + ..TaskTemplate::default() + }, + TaskTemplate { + label: format!("run '{}'", VariableName::File.template_value()), + command: "python3".to_owned(), + args: vec![VariableName::File.template_value()], + ..TaskTemplate::default() + }, + ])) +} + #[cfg(test)] mod tests { use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext}; diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 5a0dd72812..aeafca1a19 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -214,15 +214,20 @@ impl Inventory { .flat_map(|task| Some((task_source_kind.as_ref()?, task))); let mut lru_score = 0_u32; - let mut task_usage = self.last_scheduled_tasks.iter().rev().fold( - HashMap::default(), - |mut tasks, (task_source_kind, resolved_task)| { - tasks - .entry(&resolved_task.id) - .or_insert_with(|| (task_source_kind, resolved_task, post_inc(&mut lru_score))); - tasks - }, - ); + let mut task_usage = self + .last_scheduled_tasks + .iter() + .rev() + .filter(|(_, task)| !task.original_task().ignore_previously_resolved) + .fold( + HashMap::default(), + |mut tasks, (task_source_kind, resolved_task)| { + tasks.entry(&resolved_task.id).or_insert_with(|| { + (task_source_kind, resolved_task, post_inc(&mut lru_score)) + }); + tasks + }, + ); let not_used_score = post_inc(&mut lru_score); let current_resolved_tasks = self .sources diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index e19fadeeab..4784cf6568 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -44,6 +44,7 @@ impl Project { .unwrap_or_else(|| Path::new("")); let (spawn_task, shell) = if let Some(spawn_task) = spawn_task { + log::debug!("Spawning task: {spawn_task:?}"); env.extend(spawn_task.env); // Activate minimal Python virtual environment if let Some(python_settings) = &python_settings.as_option() { diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 88b0239039..2daf88b0de 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -39,6 +39,20 @@ pub struct TaskTemplate { /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish. #[serde(default)] pub allow_concurrent_runs: bool, + // Tasks like "execute the selection" better have the constant labels (to avoid polluting the history with temporary tasks), + // and always use the latest context with the latest selection. + // + // Current impl will always pick previously spawned tasks on full label conflict in the tasks modal and terminal tabs, never + // getting the latest selection for them. + // This flag inverts the behavior, effectively removing all previously spawned tasks from history, + // if their full labels are the same as the labels of the newly resolved tasks. + // Such tasks are still re-runnable, and will use the old context in that case (unless the rerun task forces this). + // + // Current approach is relatively hacky, a better way is understand when the new resolved tasks needs a rerun, + // and replace the historic task accordingly. + #[doc(hidden)] + #[serde(default)] + pub ignore_previously_resolved: bool, /// 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) /// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there @@ -114,12 +128,22 @@ impl TaskTemplate { } .map(PathBuf::from) .or(cx.cwd.clone()); - let shortened_label = substitute_all_template_variables_in_str( + let human_readable_label = substitute_all_template_variables_in_str( &self.label, &truncated_variables, &variable_names, &mut substituted_variables, - )?; + )? + .lines() + .fold(String::new(), |mut string, line| { + if string.is_empty() { + string.push_str(line); + } else { + string.push_str("\\n"); + string.push_str(line); + } + string + }); let full_label = substitute_all_template_variables_in_str( &self.label, &task_variables, @@ -162,7 +186,7 @@ impl TaskTemplate { id, cwd, full_label, - label: shortened_label, + label: human_readable_label, command, args, env, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 7742073c10..0b7c39ced1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -286,6 +286,7 @@ impl Display for TerminalError { } } +#[derive(Debug)] pub struct SpawnTask { pub id: TaskId, pub full_label: String, diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml index d9e3606d4d..029ac36f5f 100644 --- a/crates/terminal_view/Cargo.toml +++ b/crates/terminal_view/Cargo.toml @@ -28,6 +28,7 @@ search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +shlex.workspace = true shellexpand.workspace = true smol.workspace = true terminal.workspace = true diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 346afeda86..243af4cd57 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1,4 +1,4 @@ -use std::{ops::ControlFlow, path::PathBuf, sync::Arc}; +use std::{borrow::Cow, ops::ControlFlow, path::PathBuf, sync::Arc}; use crate::TerminalView; use collections::{HashMap, HashSet}; @@ -319,6 +319,7 @@ impl TerminalPanel { let args = std::mem::take(&mut spawn_task.args); for arg in args { command.push(' '); + let arg = shlex::try_quote(&arg).unwrap_or(Cow::Borrowed(&arg)); command.push_str(&arg); } spawn_task.command = shell;