Skip to repository content801 lines · 30.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:47:46.355Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
terminals.rs
1use anyhow::Result;
2use collections::HashMap;
3use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
4
5use async_channel::bounded;
6use futures::{FutureExt, future::Shared};
7use itertools::Itertools as _;
8use language::LanguageName;
9use remote::{Interactive, RemoteClient};
10use settings::{Settings, SettingsLocation};
11use std::{
12 borrow::Cow,
13 path::{Path, PathBuf},
14 sync::Arc,
15};
16use task::{Shell, ShellBuilder, ShellKind, SpawnInTerminal};
17use terminal::{
18 TaskState, TaskStatus, Terminal, TerminalBuilder, insert_zed_terminal_env,
19 terminal_settings::TerminalSettings,
20};
21use util::{
22 command::new_std_command, get_default_system_shell, get_system_shell, maybe, rel_path::RelPath,
23};
24
25use crate::{Project, ProjectPath};
26
27pub struct Terminals {
28 pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
29}
30
31impl Project {
32 pub fn active_entry_directory(&self, cx: &App) -> Option<PathBuf> {
33 let entry_id = self.active_entry()?;
34 let worktree = self.worktree_for_entry(entry_id, cx)?;
35 let worktree = worktree.read(cx);
36 let entry = worktree.entry_for_id(entry_id)?;
37
38 let absolute_path = worktree.absolutize(entry.path.as_ref());
39 if entry.is_dir() {
40 Some(absolute_path)
41 } else {
42 absolute_path.parent().map(|p| p.to_path_buf())
43 }
44 }
45
46 pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
47 self.active_entry()
48 .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
49 .into_iter()
50 .chain(self.worktrees(cx))
51 .find_map(|tree| tree.read(cx).root_dir())
52 }
53
54 pub fn first_project_directory(&self, cx: &App) -> Option<PathBuf> {
55 let worktree = self.worktrees(cx).next()?;
56 let worktree = worktree.read(cx);
57 if worktree.root_entry()?.is_dir() {
58 Some(worktree.abs_path().to_path_buf())
59 } else {
60 None
61 }
62 }
63
64 pub fn create_terminal_task(
65 &mut self,
66 spawn_task: SpawnInTerminal,
67 cx: &mut Context<Self>,
68 ) -> Task<Result<Entity<Terminal>>> {
69 let is_via_remote = self.remote_client.is_some();
70
71 let path: Option<Arc<Path>> = if let Some(cwd) = &spawn_task.cwd {
72 if is_via_remote {
73 Some(Arc::from(cwd.as_ref()))
74 } else {
75 let cwd = cwd.to_string_lossy();
76 let tilde_substituted = shellexpand::tilde(&cwd);
77 Some(Arc::from(Path::new(tilde_substituted.as_ref())))
78 }
79 } else {
80 self.active_project_directory(cx)
81 };
82
83 let mut settings_location = None;
84 if let Some(path) = path.as_ref()
85 && let Some((worktree, _)) = self.find_worktree(path, cx)
86 {
87 settings_location = Some(SettingsLocation {
88 worktree_id: worktree.read(cx).id(),
89 path: RelPath::empty(),
90 });
91 }
92 let settings = TerminalSettings::get(settings_location, cx).clone();
93 let detect_venv = settings.detect_venv.as_option().is_some();
94
95 let (completion_tx, completion_rx) = bounded(1);
96
97 let local_path = if is_via_remote { None } else { path.clone() };
98 let task_state = Some(TaskState {
99 spawned_task: spawn_task.clone(),
100 status: TaskStatus::Running,
101 completion_rx,
102 });
103 let remote_client = self.remote_client.clone();
104 let shell = match &remote_client {
105 Some(remote_client) => remote_client
106 .read(cx)
107 .shell()
108 .unwrap_or_else(get_default_system_shell),
109 None => get_system_shell(),
110 };
111 let path_style = self.path_style(cx);
112 let shell_kind = ShellKind::new(&shell, path_style.is_windows());
113
114 // Prepare a task for resolving the environment
115 let env_task =
116 self.resolve_directory_environment(&shell, path.clone(), remote_client.clone(), cx);
117
118 // Scope the toolchain lookup to the worktree the terminal is being
119 // spawned in. Previously this iterated the active editor's worktree
120 // and then every visible worktree, so a Python toolchain persisted
121 // for worktree A would leak into a terminal opened in worktree B and
122 // inject (e.g.) `conda activate base` into a shell that has no
123 // business with conda.
124 let project_path_contexts: Vec<ProjectPath> = path
125 .as_ref()
126 .and_then(|p| self.find_worktree(p, cx))
127 .map(|(worktree, relative_path)| ProjectPath {
128 worktree_id: worktree.read(cx).id(),
129 path: relative_path,
130 })
131 .into_iter()
132 .collect();
133 let toolchains = project_path_contexts
134 .into_iter()
135 .filter(|_| detect_venv)
136 .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
137 .collect::<Vec<_>>();
138 let lang_registry = self.languages.clone();
139 cx.spawn(async move |project, cx| {
140 let mut env = env_task.await.unwrap_or_default();
141 env.extend(settings.env);
142
143 let activation_script = maybe!(async {
144 for toolchain in toolchains {
145 let Some(toolchain) = toolchain.await else {
146 continue;
147 };
148 let language = lang_registry
149 .language_for_name(&toolchain.language_name.0)
150 .await
151 .ok();
152 let lister = language?.toolchain_lister()?;
153 let future =
154 cx.update(|cx| lister.activation_script(&toolchain, shell_kind, cx));
155 return Some(future.await);
156 }
157 None
158 })
159 .await
160 .unwrap_or_default();
161
162 let builder = project
163 .update(cx, move |_, cx| {
164 let format_to_run = |spawn_task: &SpawnInTerminal| {
165 format_task_for_activation(
166 spawn_task,
167 shell_kind,
168 &shell,
169 path_style.is_windows(),
170 )
171 };
172
173 let (shell, env) = {
174 let to_run =
175 (!activation_script.is_empty()).then(|| format_to_run(&spawn_task));
176 env.extend(spawn_task.env);
177 match remote_client {
178 Some(remote_client) => match activation_script.clone() {
179 activation_script if !activation_script.is_empty() => {
180 let separator = shell_kind.sequential_commands_separator();
181 let activation_script =
182 activation_script.join(&format!("{separator} "));
183 let to_run = to_run.expect("activation command was formatted");
184
185 let arg = format!("{activation_script}{separator} {to_run}");
186 let args = shell_kind.args_for_shell(true, arg);
187 let shell = remote_client
188 .read(cx)
189 .shell()
190 .unwrap_or_else(get_default_system_shell);
191
192 create_remote_shell(
193 Some((&shell, &args)),
194 env,
195 path,
196 remote_client,
197 cx,
198 )?
199 }
200 _ => create_remote_shell(
201 spawn_task
202 .command
203 .as_ref()
204 .map(|command| (command, &spawn_task.args)),
205 env,
206 path,
207 remote_client,
208 cx,
209 )?,
210 },
211 None => match activation_script.clone() {
212 activation_script if !activation_script.is_empty() => {
213 let separator = shell_kind.sequential_commands_separator();
214 let activation_script =
215 activation_script.join(&format!("{separator} "));
216 let to_run = to_run.expect("activation command was formatted");
217
218 let arg = format!("{activation_script}{separator} {to_run}");
219 let args = shell_kind.args_for_shell(true, arg);
220
221 (
222 Shell::WithArguments {
223 program: shell,
224 args,
225 title_override: None,
226 },
227 env,
228 )
229 }
230 _ => (
231 if let Some(program) = spawn_task.command {
232 Shell::WithArguments {
233 program,
234 args: spawn_task.args,
235 title_override: None,
236 }
237 } else {
238 Shell::System
239 },
240 env,
241 ),
242 },
243 }
244 };
245 anyhow::Ok(TerminalBuilder::new(
246 local_path.map(|path| path.to_path_buf()),
247 task_state,
248 shell,
249 env,
250 settings.cursor_shape,
251 settings.alternate_scroll,
252 settings.max_scroll_history_lines,
253 settings.path_hyperlink_regexes,
254 settings.path_hyperlink_timeout_ms,
255 is_via_remote,
256 cx.entity_id().as_u64(),
257 Some(completion_tx),
258 cx,
259 activation_script,
260 path_style,
261 ))
262 })??
263 .await?;
264 project.update(cx, move |this, cx| {
265 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
266
267 this.terminals
268 .local_handles
269 .push(terminal_handle.downgrade());
270
271 let id = terminal_handle.entity_id();
272 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
273 let handles = &mut project.terminals.local_handles;
274
275 if let Some(index) = handles
276 .iter()
277 .position(|terminal| terminal.entity_id() == id)
278 {
279 handles.remove(index);
280 cx.notify();
281 }
282 })
283 .detach();
284
285 terminal_handle
286 })
287 })
288 }
289
290 pub fn create_terminal_shell(
291 &mut self,
292 cwd: Option<PathBuf>,
293 cx: &mut Context<Self>,
294 ) -> Task<Result<Entity<Terminal>>> {
295 self.create_terminal_shell_internal(cwd, false, cx)
296 }
297
298 /// Creates a local terminal even if the project is remote.
299 /// In remote projects: opens in Zed's launch directory (bypasses SSH).
300 /// In local projects: opens in the project directory (same as regular terminals).
301 pub fn create_local_terminal(
302 &mut self,
303 cx: &mut Context<Self>,
304 ) -> Task<Result<Entity<Terminal>>> {
305 let working_directory = if self.remote_client.is_some() {
306 // Remote project: don't use remote paths, let shell use Zed's cwd
307 None
308 } else {
309 // Local project: use project directory like normal terminals
310 self.active_project_directory(cx).map(|p| p.to_path_buf())
311 };
312 self.create_terminal_shell_internal(working_directory, true, cx)
313 }
314
315 /// Internal method for creating terminal shells.
316 /// If force_local is true, creates a local terminal even if the project has a remote client.
317 /// This allows "breaking out" to a local shell in remote projects.
318 fn create_terminal_shell_internal(
319 &mut self,
320 cwd: Option<PathBuf>,
321 force_local: bool,
322 cx: &mut Context<Self>,
323 ) -> Task<Result<Entity<Terminal>>> {
324 let path = cwd.map(|p| Arc::from(&*p));
325 let is_via_remote = !force_local && self.remote_client.is_some();
326
327 let mut settings_location = None;
328 if let Some(path) = path.as_ref()
329 && let Some((worktree, _)) = self.find_worktree(path, cx)
330 {
331 settings_location = Some(SettingsLocation {
332 worktree_id: worktree.read(cx).id(),
333 path: RelPath::empty(),
334 });
335 }
336 let settings = TerminalSettings::get(settings_location, cx).clone();
337 let detect_venv = settings.detect_venv.as_option().is_some();
338 let local_path = if is_via_remote { None } else { path.clone() };
339
340 // See create_terminal_task: scope the toolchain lookup to the
341 // worktree the terminal is opened in, not the active editor's
342 // worktree or other visible worktrees.
343 let project_path_contexts: Vec<ProjectPath> = path
344 .as_ref()
345 .and_then(|p| self.find_worktree(p, cx))
346 .map(|(worktree, relative_path)| ProjectPath {
347 worktree_id: worktree.read(cx).id(),
348 path: relative_path,
349 })
350 .into_iter()
351 .collect();
352 let toolchains = project_path_contexts
353 .into_iter()
354 .filter(|_| detect_venv)
355 .map(|p| self.active_toolchain(p, LanguageName::new_static("Python"), cx))
356 .collect::<Vec<_>>();
357 let remote_client = if force_local {
358 None
359 } else {
360 self.remote_client.clone()
361 };
362 let shell = match &remote_client {
363 Some(remote_client) => remote_client
364 .read(cx)
365 .shell()
366 .unwrap_or_else(get_default_system_shell),
367 None => settings.shell.program(),
368 };
369 let env_shell = match &remote_client {
370 Some(_) => shell.clone(),
371 None => get_system_shell(),
372 };
373
374 let path_style = self.path_style(cx);
375
376 // Prepare a task for resolving the environment
377 let env_task =
378 self.resolve_directory_environment(&env_shell, path.clone(), remote_client.clone(), cx);
379
380 let lang_registry = self.languages.clone();
381 cx.spawn(async move |project, cx| {
382 let shell_kind = ShellKind::new(&shell, path_style.is_windows());
383 let mut env = env_task.await.unwrap_or_default();
384 env.extend(settings.env);
385
386 let activation_script = maybe!(async {
387 for toolchain in toolchains {
388 let Some(toolchain) = toolchain.await else {
389 continue;
390 };
391 let language = lang_registry
392 .language_for_name(&toolchain.language_name.0)
393 .await
394 .ok();
395 let lister = language?.toolchain_lister()?;
396 let future =
397 cx.update(|cx| lister.activation_script(&toolchain, shell_kind, cx));
398 return Some(future.await);
399 }
400 None
401 })
402 .await
403 .unwrap_or_default();
404
405 let builder = project
406 .update(cx, move |_, cx| {
407 let (shell, env) = {
408 match remote_client {
409 Some(remote_client) => {
410 create_remote_shell(None, env, path, remote_client, cx)?
411 }
412 None => (settings.shell, env),
413 }
414 };
415 anyhow::Ok(TerminalBuilder::new(
416 local_path.map(|path| path.to_path_buf()),
417 None,
418 shell,
419 env,
420 settings.cursor_shape,
421 settings.alternate_scroll,
422 settings.max_scroll_history_lines,
423 settings.path_hyperlink_regexes,
424 settings.path_hyperlink_timeout_ms,
425 is_via_remote,
426 cx.entity_id().as_u64(),
427 None,
428 cx,
429 activation_script,
430 path_style,
431 ))
432 })??
433 .await?;
434 project.update(cx, move |this, cx| {
435 let terminal_handle = cx.new(|cx| builder.subscribe(cx));
436
437 this.terminals
438 .local_handles
439 .push(terminal_handle.downgrade());
440
441 let id = terminal_handle.entity_id();
442 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
443 let handles = &mut project.terminals.local_handles;
444
445 if let Some(index) = handles
446 .iter()
447 .position(|terminal| terminal.entity_id() == id)
448 {
449 handles.remove(index);
450 cx.notify();
451 }
452 })
453 .detach();
454
455 terminal_handle
456 })
457 })
458 }
459
460 pub fn clone_terminal(
461 &mut self,
462 terminal: &Entity<Terminal>,
463 cx: &mut Context<'_, Project>,
464 cwd: Option<PathBuf>,
465 ) -> Task<Result<Entity<Terminal>>> {
466 // We cannot clone the task's terminal, as it will effectively re-spawn the task, which might not be desirable.
467 // For now, create a new shell instead.
468 if terminal.read(cx).task().is_some() {
469 return self.create_terminal_shell(cwd, cx);
470 }
471 let local_path = if self.is_via_remote_server() {
472 None
473 } else {
474 cwd
475 };
476
477 let builder = terminal.read(cx).clone_builder(cx, local_path);
478 cx.spawn(async |project, cx| {
479 let terminal = builder.await?;
480 project.update(cx, |project, cx| {
481 let terminal_handle = cx.new(|cx| terminal.subscribe(cx));
482
483 project
484 .terminals
485 .local_handles
486 .push(terminal_handle.downgrade());
487
488 let id = terminal_handle.entity_id();
489 cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
490 let handles = &mut project.terminals.local_handles;
491
492 if let Some(index) = handles
493 .iter()
494 .position(|terminal| terminal.entity_id() == id)
495 {
496 handles.remove(index);
497 cx.notify();
498 }
499 })
500 .detach();
501
502 terminal_handle
503 })
504 })
505 }
506
507 pub fn terminal_settings<'a>(
508 &'a self,
509 path: &'a Option<PathBuf>,
510 cx: &'a App,
511 ) -> &'a TerminalSettings {
512 let mut settings_location = None;
513 if let Some(path) = path.as_ref()
514 && let Some((worktree, _)) = self.find_worktree(path, cx)
515 {
516 settings_location = Some(SettingsLocation {
517 worktree_id: worktree.read(cx).id(),
518 path: RelPath::empty(),
519 });
520 }
521 TerminalSettings::get(settings_location, cx)
522 }
523
524 pub fn exec_in_shell(
525 &self,
526 command: String,
527 cx: &mut Context<Self>,
528 ) -> Task<Result<smol::process::Command>> {
529 let path = self.first_project_directory(cx);
530 let remote_client = self.remote_client.clone();
531 let settings = self.terminal_settings(&path, cx).clone();
532 let shell = remote_client
533 .as_ref()
534 .and_then(|remote_client| remote_client.read(cx).shell())
535 .map(Shell::Program)
536 .unwrap_or(Shell::System);
537 let is_windows = self.path_style(cx).is_windows();
538 let builder = ShellBuilder::new(&shell, is_windows).non_interactive();
539 let (command, args) = builder.build(Some(command), &Vec::new());
540
541 let env_task = self.resolve_directory_environment(
542 &shell.program(),
543 path.as_ref().map(|p| Arc::from(&**p)),
544 remote_client.clone(),
545 cx,
546 );
547
548 cx.spawn(async move |project, cx| {
549 let mut env = env_task.await.unwrap_or_default();
550 env.extend(settings.env);
551
552 project.update(cx, move |_, cx| {
553 match remote_client {
554 Some(remote_client) => {
555 let command_template = remote_client.read(cx).build_command(
556 Some(command),
557 &args,
558 &env,
559 None,
560 None,
561 Interactive::Yes,
562 )?;
563 let mut command = new_std_command(command_template.program);
564 command.args(command_template.args);
565 command.envs(command_template.env);
566 Ok(command)
567 }
568 None => {
569 let mut command = new_std_command(command);
570 command.args(args);
571 command.envs(env);
572 if let Some(path) = path {
573 command.current_dir(path);
574 }
575 Ok(command)
576 }
577 }
578 .map(|mut process| {
579 util::set_pre_exec_to_start_new_session(&mut process);
580 smol::process::Command::from(process)
581 })
582 })?
583 })
584 }
585
586 pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
587 &self.terminals.local_handles
588 }
589
590 fn resolve_directory_environment(
591 &self,
592 shell: &str,
593 path: Option<Arc<Path>>,
594 remote_client: Option<Entity<RemoteClient>>,
595 cx: &mut App,
596 ) -> Shared<Task<Option<HashMap<String, String>>>> {
597 if let Some(path) = &path {
598 let shell = Shell::Program(shell.to_string());
599 self.environment
600 .update(cx, |project_env, cx| match &remote_client {
601 Some(remote_client) => project_env.remote_directory_environment(
602 &shell,
603 path.clone(),
604 remote_client.clone(),
605 cx,
606 ),
607 None => project_env.local_directory_environment(&shell, path.clone(), cx),
608 })
609 } else {
610 Task::ready(None).shared()
611 }
612 }
613}
614
615fn create_remote_shell(
616 spawn_command: Option<(&String, &Vec<String>)>,
617 mut env: HashMap<String, String>,
618 working_directory: Option<Arc<Path>>,
619 remote_client: Entity<RemoteClient>,
620 cx: &mut App,
621) -> Result<(Shell, HashMap<String, String>)> {
622 insert_zed_terminal_env(&mut env, &release_channel::AppVersion::global(cx));
623
624 let (program, args) = match spawn_command {
625 Some((program, args)) => (Some(program.clone()), args),
626 None => (None, &Vec::new()),
627 };
628
629 let command = remote_client.read(cx).build_command(
630 program,
631 args.as_slice(),
632 &env,
633 working_directory.map(|path| path.display().to_string()),
634 None,
635 Interactive::Yes,
636 )?;
637
638 log::debug!("Connecting to a remote server: {:?}", command.program);
639 let host = remote_client.read(cx).connection_options().display_name();
640
641 Ok((
642 Shell::WithArguments {
643 program: command.program,
644 args: command.args,
645 title_override: Some(format!("{} — Terminal", host)),
646 },
647 command.env,
648 ))
649}
650
651fn format_task_for_activation(
652 spawn_task: &SpawnInTerminal,
653 shell_kind: ShellKind,
654 shell: &str,
655 is_windows: bool,
656) -> String {
657 if let Some(command) = &spawn_task.command {
658 let command = shell_kind.prepend_command_prefix(command);
659 let command = shell_kind.try_quote_prefix_aware(&command);
660 let args = spawn_task
661 .args
662 .iter()
663 .enumerate()
664 .filter_map(|(index, arg)| {
665 quote_prepared_task_arg_for_activation(
666 spawn_task, shell_kind, arg, index, is_windows,
667 )
668 });
669
670 command.into_iter().chain(args).join(" ")
671 } else {
672 // todo: this breaks for remotes to windows
673 format!("exec {shell} -l")
674 }
675}
676
677fn quote_prepared_task_arg_for_activation<'a>(
678 spawn_task: &SpawnInTerminal,
679 shell_kind: ShellKind,
680 arg: &'a str,
681 index: usize,
682 is_windows: bool,
683) -> Option<Cow<'a, str>> {
684 if spawn_task.shell.shell_kind(is_windows) == ShellKind::Cmd
685 && index >= 2
686 && spawn_task
687 .args
688 .get(index - 2)
689 .is_some_and(|arg| arg.eq_ignore_ascii_case("/S"))
690 && spawn_task
691 .args
692 .get(index - 1)
693 .is_some_and(|arg| arg.eq_ignore_ascii_case("/C"))
694 {
695 // The /C argument is already a cmd command string from prepare_task_for_spawn.
696 // Quoting it again for venv activation makes cmd see the quotes as literals.
697 return quote_cmd_command_arg_for_outer_shell(arg, shell_kind).map(Cow::Owned);
698 }
699
700 shell_kind.try_quote(arg)
701}
702
703fn quote_cmd_command_arg_for_outer_shell(arg: &str, shell_kind: ShellKind) -> Option<String> {
704 match shell_kind {
705 ShellKind::PowerShell | ShellKind::Pwsh => Some(format!("'{}'", arg.replace('\'', "''"))),
706 ShellKind::Cmd => Some(arg.to_string()),
707 ShellKind::Posix
708 | ShellKind::Csh
709 | ShellKind::Tcsh
710 | ShellKind::Fish
711 | ShellKind::Nushell
712 | ShellKind::Rc
713 | ShellKind::Xonsh
714 | ShellKind::Elvish => shell_kind.try_quote(arg).map(Cow::into_owned),
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721 use pretty_assertions::assert_eq;
722
723 fn prepared_cmd_task(command_arg: &str) -> SpawnInTerminal {
724 SpawnInTerminal {
725 command: Some("cmd.exe".to_string()),
726 args: vec!["/S".to_string(), "/C".to_string(), command_arg.to_string()],
727 shell: Shell::Program("cmd.exe".to_string()),
728 ..SpawnInTerminal::default()
729 }
730 }
731
732 #[test]
733 fn formats_prepared_cmd_task_for_powershell_activation() {
734 let task = prepared_cmd_task("\"echo Hi there\"");
735
736 assert_eq!(
737 format_task_for_activation(&task, ShellKind::PowerShell, "powershell.exe", true),
738 "&cmd.exe /S /C '\"echo Hi there\"'"
739 );
740 }
741
742 #[test]
743 fn formats_prepared_cmd_task_for_cmd_activation() {
744 let task = prepared_cmd_task("\"echo Hi there\"");
745
746 assert_eq!(
747 format_task_for_activation(&task, ShellKind::Cmd, "cmd.exe", true),
748 "cmd.exe /S /C \"echo Hi there\""
749 );
750 }
751
752 #[test]
753 fn formats_prepared_cmd_task_with_shell_args_for_activation() {
754 let task = SpawnInTerminal {
755 command: Some("cmd.exe".to_string()),
756 args: vec![
757 "/D".to_string(),
758 "/S".to_string(),
759 "/C".to_string(),
760 "\"echo Hi there\"".to_string(),
761 ],
762 shell: Shell::WithArguments {
763 program: "cmd.exe".to_string(),
764 args: vec!["/D".to_string()],
765 title_override: None,
766 },
767 ..SpawnInTerminal::default()
768 };
769
770 assert_eq!(
771 format_task_for_activation(&task, ShellKind::PowerShell, "powershell.exe", true),
772 "&cmd.exe /D /S /C '\"echo Hi there\"'"
773 );
774 }
775
776 #[test]
777 fn formats_prepared_cmd_task_with_single_quote_for_powershell_activation() {
778 let task = prepared_cmd_task("\"echo It's fine\"");
779
780 assert_eq!(
781 format_task_for_activation(&task, ShellKind::PowerShell, "powershell.exe", true),
782 "&cmd.exe /S /C '\"echo It''s fine\"'"
783 );
784 }
785
786 #[test]
787 fn formats_non_cmd_task_for_activation() {
788 let task = SpawnInTerminal {
789 command: Some("cargo".to_string()),
790 args: vec!["test".to_string(), "some test".to_string()],
791 shell: Shell::System,
792 ..SpawnInTerminal::default()
793 };
794
795 assert_eq!(
796 format_task_for_activation(&task, ShellKind::PowerShell, "powershell.exe", true),
797 "&cargo test 'some test'"
798 );
799 }
800}
801