255 lines · 8.2 KB · Rust
RawDownload
use agent_client_protocol as acp;use anyhow::Result;use futures::{FutureExt as _, future::Shared};use gpui::{App, AppContext, AsyncApp, Context, Entity, Task};use language::LanguageRegistry;use markdown::Markdown;use project::Project;use std::{    path::PathBuf,    process::ExitStatus,    sync::{        Arc,        atomic::{AtomicBool, Ordering},    },    time::Instant,};use task::Shell;use util::get_default_system_shell_preferring_bash; pub struct Terminal {    id: acp::TerminalId,    command: Entity<Markdown>,    working_dir: Option<PathBuf>,    terminal: Entity<terminal::Terminal>,    started_at: Instant,    output: Option<TerminalOutput>,    output_byte_limit: Option<usize>,    _output_task: Shared<Task<acp::TerminalExitStatus>>,    /// Flag indicating whether this terminal was stopped by explicit user action    /// (e.g., clicking the Stop button). This is set before kill() is called    /// so that code awaiting wait_for_exit() can check it deterministically.    user_stopped: Arc<AtomicBool>,} pub struct TerminalOutput {    pub ended_at: Instant,    pub exit_status: Option<ExitStatus>,    pub content: String,    pub original_content_len: usize,    pub content_line_count: usize,} impl Terminal {    pub fn new(        id: acp::TerminalId,        command_label: &str,        working_dir: Option<PathBuf>,        output_byte_limit: Option<usize>,        terminal: Entity<terminal::Terminal>,        language_registry: Arc<LanguageRegistry>,        cx: &mut Context<Self>,    ) -> Self {        let command_task = terminal.read(cx).wait_for_completed_task(cx);        Self {            id,            command: cx.new(|cx| {                Markdown::new(                    format!("```\n{}\n```", command_label).into(),                    Some(language_registry.clone()),                    None,                    cx,                )            }),            working_dir,            terminal,            started_at: Instant::now(),            output: None,            output_byte_limit,            user_stopped: Arc::new(AtomicBool::new(false)),            _output_task: cx                .spawn(async move |this, cx| {                    let exit_status = command_task.await;                     this.update(cx, |this, cx| {                        let (content, original_content_len) = this.truncated_output(cx);                        let content_line_count = this.terminal.read(cx).total_lines();                         this.output = Some(TerminalOutput {                            ended_at: Instant::now(),                            exit_status,                            content,                            original_content_len,                            content_line_count,                        });                        cx.notify();                    })                    .ok();                     let exit_status = exit_status.map(portable_pty::ExitStatus::from);                     acp::TerminalExitStatus::new()                        .exit_code(exit_status.as_ref().map(|e| e.exit_code()))                        .signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned)))                })                .shared(),        }    }     pub fn id(&self) -> &acp::TerminalId {        &self.id    }     pub fn wait_for_exit(&self) -> Shared<Task<acp::TerminalExitStatus>> {        self._output_task.clone()    }     pub fn kill(&mut self, cx: &mut App) {        self.terminal.update(cx, |terminal, _cx| {            terminal.kill_active_task();        });    }     /// Marks this terminal as stopped by user action and then kills it.    /// This should be called when the user explicitly clicks a Stop button.    pub fn stop_by_user(&mut self, cx: &mut App) {        self.user_stopped.store(true, Ordering::SeqCst);        self.kill(cx);    }     /// Returns whether this terminal was stopped by explicit user action.    pub fn was_stopped_by_user(&self) -> bool {        self.user_stopped.load(Ordering::SeqCst)    }     pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {        if let Some(output) = self.output.as_ref() {            let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);             acp::TerminalOutputResponse::new(                output.content.clone(),                output.original_content_len > output.content.len(),            )            .exit_status(                acp::TerminalExitStatus::new()                    .exit_code(exit_status.as_ref().map(|e| e.exit_code()))                    .signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned))),            )        } else {            let (current_content, original_len) = self.truncated_output(cx);            let truncated = current_content.len() < original_len;            acp::TerminalOutputResponse::new(current_content, truncated)        }    }     fn truncated_output(&self, cx: &App) -> (String, usize) {        let terminal = self.terminal.read(cx);        let mut content = terminal.get_content();         let original_content_len = content.len();         if let Some(limit) = self.output_byte_limit            && content.len() > limit        {            let mut end_ix = limit.min(content.len());            while !content.is_char_boundary(end_ix) {                end_ix -= 1;            }            // Don't truncate mid-line, clear the remainder of the last line            end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix);            content.truncate(end_ix);        }         (content, original_content_len)    }     pub fn command(&self) -> &Entity<Markdown> {        &self.command    }     pub fn update_command_label(&self, label: &str, cx: &mut App) {        self.command.update(cx, |command, cx| {            command.replace(format!("```\n{}\n```", label), cx);        });    }     pub fn working_dir(&self) -> &Option<PathBuf> {        &self.working_dir    }     pub fn started_at(&self) -> Instant {        self.started_at    }     pub fn output(&self) -> Option<&TerminalOutput> {        self.output.as_ref()    }     pub fn inner(&self) -> &Entity<terminal::Terminal> {        &self.terminal    }     pub fn to_markdown(&self, cx: &App) -> String {        format!(            "Terminal:\n```\n{}\n```\n",            self.terminal.read(cx).get_content()        )    }} pub async fn create_terminal_entity(    command: String,    args: &[String],    env_vars: Vec<(String, String)>,    cwd: Option<PathBuf>,    project: &Entity<Project>,    cx: &mut AsyncApp,) -> Result<Entity<terminal::Terminal>> {    let mut env = if let Some(dir) = &cwd {        project            .update(cx, |project, cx| {                project.environment().update(cx, |env, cx| {                    env.directory_environment(dir.clone().into(), cx)                })            })            .await            .unwrap_or_default()    } else {        Default::default()    };     // Disable pagers so agent/terminal commands don't hang behind interactive UIs    env.insert("PAGER".into(), "".into());    // Override user core.pager (e.g. delta) which Git prefers over PAGER    env.insert("GIT_PAGER".into(), "cat".into());    env.extend(env_vars);     // Use remote shell or default system shell, as appropriate    let shell = project        .update(cx, |project, cx| {            project                .remote_client()                .and_then(|r| r.read(cx).default_system_shell())                .map(Shell::Program)        })        .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));    let is_windows = project.read_with(cx, |project, cx| project.path_style(cx).is_windows());    let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)        .redirect_stdin_to_dev_null()        .build(Some(command.clone()), &args);     project        .update(cx, |project, cx| {            project.create_terminal_task(                task::SpawnInTerminal {                    command: Some(task_command),                    args: task_args,                    cwd,                    env,                    ..Default::default()                },                cx,            )        })        .await}