terminals.rs

  1use anyhow::Result;
  2use collections::HashMap;
  3use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
  4
  5use futures::{FutureExt, future::Shared};
  6use itertools::Itertools as _;
  7use language::LanguageName;
  8use remote::RemoteClient;
  9use settings::{Settings, SettingsLocation};
 10use smol::channel::bounded;
 11use std::{
 12    path::{Path, PathBuf},
 13    sync::Arc,
 14};
 15use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
 16use terminal::{
 17    TaskState, TaskStatus, Terminal, TerminalBuilder, insert_zed_terminal_env,
 18    terminal_settings::TerminalSettings,
 19};
 20use util::{command::new_std_command, get_default_system_shell, maybe, rel_path::RelPath};
 21
 22use crate::{Project, ProjectPath};
 23
 24pub struct Terminals {
 25    pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
 26}
 27
 28impl Project {
 29    pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
 30        self.active_entry()
 31            .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
 32            .into_iter()
 33            .chain(self.worktrees(cx))
 34            .find_map(|tree| tree.read(cx).root_dir())
 35    }
 36
 37    pub fn first_project_directory(&self, cx: &App) -> Option<PathBuf> {
 38        let worktree = self.worktrees(cx).next()?;
 39        let worktree = worktree.read(cx);
 40        if worktree.root_entry()?.is_dir() {
 41            Some(worktree.abs_path().to_path_buf())
 42        } else {
 43            None
 44        }
 45    }
 46
 47    pub fn create_terminal_task(
 48        &mut self,
 49        spawn_task: SpawnInTerminal,
 50        cx: &mut Context<Self>,
 51    ) -> Task<Result<Entity<Terminal>>> {
 52        let is_via_remote = self.remote_client.is_some();
 53
 54        let path: Option<Arc<Path>> = if let Some(cwd) = &spawn_task.cwd {
 55            if is_via_remote {
 56                Some(Arc::from(cwd.as_ref()))
 57            } else {
 58                let cwd = cwd.to_string_lossy();
 59                let tilde_substituted = shellexpand::tilde(&cwd);
 60                Some(Arc::from(Path::new(tilde_substituted.as_ref())))
 61            }
 62        } else {
 63            self.active_project_directory(cx)
 64        };
 65
 66        let mut settings_location = None;
 67        if let Some(path) = path.as_ref()
 68            && let Some((worktree, _)) = self.find_worktree(path, cx)
 69        {
 70            settings_location = Some(SettingsLocation {
 71                worktree_id: worktree.read(cx).id(),
 72                path: RelPath::empty(),
 73            });
 74        }
 75        let settings = TerminalSettings::get(settings_location, cx).clone();
 76        let detect_venv = settings.detect_venv.as_option().is_some();
 77
 78        let (completion_tx, completion_rx) = bounded(1);
 79
 80        let local_path = if is_via_remote { None } else { path.clone() };
 81        let task_state = Some(TaskState {
 82            spawned_task: spawn_task.clone(),
 83            status: TaskStatus::Running,
 84            completion_rx,
 85        });
 86        let remote_client = self.remote_client.clone();
 87        let shell = match &remote_client {
 88            Some(remote_client) => remote_client
 89                .read(cx)
 90                .shell()
 91                .unwrap_or_else(get_default_system_shell),
 92            None => settings.shell.program(),
 93        };
 94        let is_windows = self.path_style(cx).is_windows();
 95        let shell_kind = ShellKind::new(&shell, is_windows);
 96
 97        // Prepare a task for resolving the environment
 98        let env_task =
 99            self.resolve_directory_environment(&shell, path.clone(), remote_client.clone(), cx);
100
101        let project_path_contexts = self
102            .active_entry()
103            .and_then(|entry_id| self.path_for_entry(entry_id, cx))
104            .into_iter()
105            .chain(
106                self.visible_worktrees(cx)
107                    .map(|wt| wt.read(cx).id())
108                    .map(|worktree_id| ProjectPath {
109                        worktree_id,
110                        path: Arc::from(RelPath::empty()),
111                    }),
112            );
113        let toolchains = project_path_contexts
114            .filter(|_| detect_venv)
115            .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
116            .collect::<Vec<_>>();
117        let lang_registry = self.languages.clone();
118        cx.spawn(async move |project, cx| {
119            let mut env = env_task.await.unwrap_or_default();
120            env.extend(settings.env);
121
122            let activation_script = maybe!(async {
123                for toolchain in toolchains {
124                    let Some(toolchain) = toolchain.await else {
125                        continue;
126                    };
127                    let language = lang_registry
128                        .language_for_name(&toolchain.language_name.0)
129                        .await
130                        .ok();
131                    let lister = language?.toolchain_lister()?;
132                    return cx
133                        .update(|cx| lister.activation_script(&toolchain, shell_kind, cx))
134                        .ok();
135                }
136                None
137            })
138            .await
139            .unwrap_or_default();
140
141            let builder = project
142                .update(cx, move |_, cx| {
143                    let format_to_run = || {
144                        if let Some(command) = &spawn_task.command {
145                            let command = shell_kind.prepend_command_prefix(command);
146                            let command = shell_kind.try_quote_prefix_aware(&command);
147                            let args = spawn_task
148                                .args
149                                .iter()
150                                .filter_map(|arg| shell_kind.try_quote(&arg));
151
152                            command.into_iter().chain(args).join(" ")
153                        } else {
154                            // todo: this breaks for remotes to windows
155                            format!("exec {shell} -l")
156                        }
157                    };
158
159                    let (shell, env) = {
160                        env.extend(spawn_task.env);
161                        match remote_client {
162                            Some(remote_client) => match activation_script.clone() {
163                                activation_script if !activation_script.is_empty() => {
164                                    let separator = shell_kind.sequential_commands_separator();
165                                    let activation_script =
166                                        activation_script.join(&format!("{separator} "));
167                                    let to_run = format_to_run();
168
169                                    let arg = format!("{activation_script}{separator} {to_run}");
170                                    let args = shell_kind.args_for_shell(false, arg);
171                                    let shell = remote_client
172                                        .read(cx)
173                                        .shell()
174                                        .unwrap_or_else(get_default_system_shell);
175
176                                    create_remote_shell(
177                                        Some((&shell, &args)),
178                                        env,
179                                        path,
180                                        remote_client,
181                                        cx,
182                                    )?
183                                }
184                                _ => create_remote_shell(
185                                    spawn_task
186                                        .command
187                                        .as_ref()
188                                        .map(|command| (command, &spawn_task.args)),
189                                    env,
190                                    path,
191                                    remote_client,
192                                    cx,
193                                )?,
194                            },
195                            None => match activation_script.clone() {
196                                activation_script if !activation_script.is_empty() => {
197                                    let separator = shell_kind.sequential_commands_separator();
198                                    let activation_script =
199                                        activation_script.join(&format!("{separator} "));
200                                    let to_run = format_to_run();
201
202                                    let mut arg =
203                                        format!("{activation_script}{separator} {to_run}");
204                                    if shell_kind == ShellKind::Cmd {
205                                        // We need to put the entire command in quotes since otherwise CMD tries to execute them
206                                        // as separate commands rather than chaining one after another.
207                                        arg = format!("\"{arg}\"");
208                                    }
209
210                                    let args = shell_kind.args_for_shell(false, arg);
211
212                                    (
213                                        Shell::WithArguments {
214                                            program: shell,
215                                            args,
216                                            title_override: None,
217                                        },
218                                        env,
219                                    )
220                                }
221                                _ => (
222                                    if let Some(program) = spawn_task.command {
223                                        Shell::WithArguments {
224                                            program,
225                                            args: spawn_task.args,
226                                            title_override: None,
227                                        }
228                                    } else {
229                                        Shell::System
230                                    },
231                                    env,
232                                ),
233                            },
234                        }
235                    };
236                    anyhow::Ok(TerminalBuilder::new(
237                        local_path.map(|path| path.to_path_buf()),
238                        task_state,
239                        shell,
240                        env,
241                        settings.cursor_shape,
242                        settings.alternate_scroll,
243                        settings.max_scroll_history_lines,
244                        settings.path_hyperlink_regexes,
245                        settings.path_hyperlink_timeout_ms,
246                        is_via_remote,
247                        cx.entity_id().as_u64(),
248                        Some(completion_tx),
249                        cx,
250                        activation_script,
251                    ))
252                })??
253                .await?;
254            project.update(cx, move |this, cx| {
255                let terminal_handle = cx.new(|cx| builder.subscribe(cx));
256
257                this.terminals
258                    .local_handles
259                    .push(terminal_handle.downgrade());
260
261                let id = terminal_handle.entity_id();
262                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
263                    let handles = &mut project.terminals.local_handles;
264
265                    if let Some(index) = handles
266                        .iter()
267                        .position(|terminal| terminal.entity_id() == id)
268                    {
269                        handles.remove(index);
270                        cx.notify();
271                    }
272                })
273                .detach();
274
275                terminal_handle
276            })
277        })
278    }
279
280    pub fn create_terminal_shell(
281        &mut self,
282        cwd: Option<PathBuf>,
283        cx: &mut Context<Self>,
284    ) -> Task<Result<Entity<Terminal>>> {
285        let path = cwd.map(|p| Arc::from(&*p));
286        let is_via_remote = self.remote_client.is_some();
287
288        let mut settings_location = None;
289        if let Some(path) = path.as_ref()
290            && let Some((worktree, _)) = self.find_worktree(path, cx)
291        {
292            settings_location = Some(SettingsLocation {
293                worktree_id: worktree.read(cx).id(),
294                path: RelPath::empty(),
295            });
296        }
297        let settings = TerminalSettings::get(settings_location, cx).clone();
298        let detect_venv = settings.detect_venv.as_option().is_some();
299        let local_path = if is_via_remote { None } else { path.clone() };
300
301        let project_path_contexts = self
302            .active_entry()
303            .and_then(|entry_id| self.path_for_entry(entry_id, cx))
304            .into_iter()
305            .chain(
306                self.visible_worktrees(cx)
307                    .map(|wt| wt.read(cx).id())
308                    .map(|worktree_id| ProjectPath {
309                        worktree_id,
310                        path: RelPath::empty().into(),
311                    }),
312            );
313        let toolchains = project_path_contexts
314            .filter(|_| detect_venv)
315            .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
316            .collect::<Vec<_>>();
317        let remote_client = self.remote_client.clone();
318        let shell = match &remote_client {
319            Some(remote_client) => remote_client
320                .read(cx)
321                .shell()
322                .unwrap_or_else(get_default_system_shell),
323            None => settings.shell.program(),
324        };
325
326        let is_windows = self.path_style(cx).is_windows();
327
328        // Prepare a task for resolving the environment
329        let env_task =
330            self.resolve_directory_environment(&shell, path.clone(), remote_client.clone(), cx);
331
332        let lang_registry = self.languages.clone();
333        cx.spawn(async move |project, cx| {
334            let shell_kind = ShellKind::new(&shell, is_windows);
335            let mut env = env_task.await.unwrap_or_default();
336            env.extend(settings.env);
337
338            let activation_script = maybe!(async {
339                for toolchain in toolchains {
340                    let Some(toolchain) = toolchain.await else {
341                        continue;
342                    };
343                    let language = lang_registry
344                        .language_for_name(&toolchain.language_name.0)
345                        .await
346                        .ok();
347                    let lister = language?.toolchain_lister()?;
348                    return cx
349                        .update(|cx| lister.activation_script(&toolchain, shell_kind, cx))
350                        .ok();
351                }
352                None
353            })
354            .await
355            .unwrap_or_default();
356
357            let builder = project
358                .update(cx, move |_, cx| {
359                    let (shell, env) = {
360                        match remote_client {
361                            Some(remote_client) => {
362                                create_remote_shell(None, env, path, remote_client, cx)?
363                            }
364                            None => (settings.shell, env),
365                        }
366                    };
367                    anyhow::Ok(TerminalBuilder::new(
368                        local_path.map(|path| path.to_path_buf()),
369                        None,
370                        shell,
371                        env,
372                        settings.cursor_shape,
373                        settings.alternate_scroll,
374                        settings.max_scroll_history_lines,
375                        settings.path_hyperlink_regexes,
376                        settings.path_hyperlink_timeout_ms,
377                        is_via_remote,
378                        cx.entity_id().as_u64(),
379                        None,
380                        cx,
381                        activation_script,
382                    ))
383                })??
384                .await?;
385            project.update(cx, move |this, cx| {
386                let terminal_handle = cx.new(|cx| builder.subscribe(cx));
387
388                this.terminals
389                    .local_handles
390                    .push(terminal_handle.downgrade());
391
392                let id = terminal_handle.entity_id();
393                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
394                    let handles = &mut project.terminals.local_handles;
395
396                    if let Some(index) = handles
397                        .iter()
398                        .position(|terminal| terminal.entity_id() == id)
399                    {
400                        handles.remove(index);
401                        cx.notify();
402                    }
403                })
404                .detach();
405
406                terminal_handle
407            })
408        })
409    }
410
411    pub fn clone_terminal(
412        &mut self,
413        terminal: &Entity<Terminal>,
414        cx: &mut Context<'_, Project>,
415        cwd: Option<PathBuf>,
416    ) -> Task<Result<Entity<Terminal>>> {
417        // We cannot clone the task's terminal, as it will effectively re-spawn the task, which might not be desirable.
418        // For now, create a new shell instead.
419        if terminal.read(cx).task().is_some() {
420            return self.create_terminal_shell(cwd, cx);
421        }
422        let local_path = if self.is_via_remote_server() {
423            None
424        } else {
425            cwd
426        };
427
428        let builder = terminal.read(cx).clone_builder(cx, local_path);
429        cx.spawn(async |project, cx| {
430            let terminal = builder.await?;
431            project.update(cx, |project, cx| {
432                let terminal_handle = cx.new(|cx| terminal.subscribe(cx));
433
434                project
435                    .terminals
436                    .local_handles
437                    .push(terminal_handle.downgrade());
438
439                let id = terminal_handle.entity_id();
440                cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
441                    let handles = &mut project.terminals.local_handles;
442
443                    if let Some(index) = handles
444                        .iter()
445                        .position(|terminal| terminal.entity_id() == id)
446                    {
447                        handles.remove(index);
448                        cx.notify();
449                    }
450                })
451                .detach();
452
453                terminal_handle
454            })
455        })
456    }
457
458    pub fn terminal_settings<'a>(
459        &'a self,
460        path: &'a Option<PathBuf>,
461        cx: &'a App,
462    ) -> &'a TerminalSettings {
463        let mut settings_location = None;
464        if let Some(path) = path.as_ref()
465            && let Some((worktree, _)) = self.find_worktree(path, cx)
466        {
467            settings_location = Some(SettingsLocation {
468                worktree_id: worktree.read(cx).id(),
469                path: RelPath::empty(),
470            });
471        }
472        TerminalSettings::get(settings_location, cx)
473    }
474
475    pub fn exec_in_shell(
476        &self,
477        command: String,
478        cx: &mut Context<Self>,
479    ) -> Task<Result<smol::process::Command>> {
480        let path = self.first_project_directory(cx);
481        let remote_client = self.remote_client.clone();
482        let settings = self.terminal_settings(&path, cx).clone();
483        let shell = remote_client
484            .as_ref()
485            .and_then(|remote_client| remote_client.read(cx).shell())
486            .map(Shell::Program)
487            .unwrap_or_else(|| settings.shell.clone());
488        let is_windows = self.path_style(cx).is_windows();
489        let builder = ShellBuilder::new(&shell, is_windows).non_interactive();
490        let (command, args) = builder.build(Some(command), &Vec::new());
491
492        let env_task = self.resolve_directory_environment(
493            &shell.program(),
494            path.as_ref().map(|p| Arc::from(&**p)),
495            remote_client.clone(),
496            cx,
497        );
498
499        cx.spawn(async move |project, cx| {
500            let mut env = env_task.await.unwrap_or_default();
501            env.extend(settings.env);
502
503            project.update(cx, move |_, cx| {
504                match remote_client {
505                    Some(remote_client) => {
506                        let command_template = remote_client.read(cx).build_command(
507                            Some(command),
508                            &args,
509                            &env,
510                            None,
511                            None,
512                        )?;
513                        let mut command = new_std_command(command_template.program);
514                        command.args(command_template.args);
515                        command.envs(command_template.env);
516                        Ok(command)
517                    }
518                    None => {
519                        let mut command = new_std_command(command);
520                        command.args(args);
521                        command.envs(env);
522                        if let Some(path) = path {
523                            command.current_dir(path);
524                        }
525                        Ok(command)
526                    }
527                }
528                .map(|mut process| {
529                    util::set_pre_exec_to_start_new_session(&mut process);
530                    smol::process::Command::from(process)
531                })
532            })?
533        })
534    }
535
536    pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
537        &self.terminals.local_handles
538    }
539
540    fn resolve_directory_environment(
541        &self,
542        shell: &str,
543        path: Option<Arc<Path>>,
544        remote_client: Option<Entity<RemoteClient>>,
545        cx: &mut App,
546    ) -> Shared<Task<Option<HashMap<String, String>>>> {
547        if let Some(path) = &path {
548            let shell = Shell::Program(shell.to_string());
549            self.environment
550                .update(cx, |project_env, cx| match &remote_client {
551                    Some(remote_client) => project_env.remote_directory_environment(
552                        &shell,
553                        path.clone(),
554                        remote_client.clone(),
555                        cx,
556                    ),
557                    None => project_env.local_directory_environment(&shell, path.clone(), cx),
558                })
559        } else {
560            Task::ready(None).shared()
561        }
562    }
563}
564
565fn create_remote_shell(
566    spawn_command: Option<(&String, &Vec<String>)>,
567    mut env: HashMap<String, String>,
568    working_directory: Option<Arc<Path>>,
569    remote_client: Entity<RemoteClient>,
570    cx: &mut App,
571) -> Result<(Shell, HashMap<String, String>)> {
572    insert_zed_terminal_env(&mut env, &release_channel::AppVersion::global(cx));
573
574    let (program, args) = match spawn_command {
575        Some((program, args)) => (Some(program.clone()), args),
576        None => (None, &Vec::new()),
577    };
578
579    let command = remote_client.read(cx).build_command(
580        program,
581        args.as_slice(),
582        &env,
583        working_directory.map(|path| path.display().to_string()),
584        None,
585    )?;
586
587    log::debug!("Connecting to a remote server: {:?}", command.program);
588    let host = remote_client.read(cx).connection_options().display_name();
589
590    Ok((
591        Shell::WithArguments {
592            program: command.program,
593            args: command.args,
594            title_override: Some(format!("{} — Terminal", host)),
595        },
596        command.env,
597    ))
598}