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}