1mod reliability;
2mod zed;
3
4use agent_ui::AgentPanel;
5use anyhow::{Context as _, Error, Result};
6use clap::{Parser, command};
7use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
8use client::{Client, ProxySettings, UserStore, parse_zed_link};
9use collab_ui::channel_view::ChannelView;
10use collections::HashMap;
11use crashes::InitCrashHandler;
12use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
13use editor::Editor;
14use extension::ExtensionHostProxy;
15use extension_host::ExtensionStore;
16use fs::{Fs, RealFs};
17use futures::{StreamExt, channel::oneshot, future};
18use git::GitHostingProviderRegistry;
19use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, UpdateGlobal as _};
20
21use gpui_tokio::Tokio;
22use http_client::{Url, read_proxy_from_env};
23use language::LanguageRegistry;
24use onboarding::{FIRST_OPEN, show_onboarding_view};
25use prompt_store::PromptBuilder;
26use remote::RemoteConnectionOptions;
27use reqwest_client::ReqwestClient;
28
29use assets::Assets;
30use node_runtime::{NodeBinaryOptions, NodeRuntime};
31use parking_lot::Mutex;
32use project::project_settings::ProjectSettings;
33use recent_projects::{SshSettings, open_remote_project};
34use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
35use session::{AppSession, Session};
36use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file};
37use std::{
38 env,
39 io::{self, IsTerminal},
40 path::{Path, PathBuf},
41 process,
42 sync::Arc,
43};
44use theme::{
45 ActiveTheme, IconThemeNotFoundError, SystemAppearance, ThemeNotFoundError, ThemeRegistry,
46 ThemeSettings,
47};
48use util::{ResultExt, TryFutureExt, maybe};
49use uuid::Uuid;
50use workspace::{
51 AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings,
52 WorkspaceStore, notifications::NotificationId,
53};
54use zed::{
55 OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options,
56 derive_paths_with_position, edit_prediction_registry, handle_cli_connection,
57 handle_keymap_file_changes, handle_settings_changed, handle_settings_file_changes,
58 initialize_workspace, open_paths_with_positions,
59};
60
61use crate::zed::OpenRequestKind;
62
63#[cfg(feature = "mimalloc")]
64#[global_allocator]
65static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
66
67fn files_not_created_on_launch(errors: HashMap<io::ErrorKind, Vec<&Path>>) {
68 let message = "Zed failed to launch";
69 let error_details = errors
70 .into_iter()
71 .flat_map(|(kind, paths)| {
72 #[allow(unused_mut)] // for non-unix platforms
73 let mut error_kind_details = match paths.len() {
74 0 => return None,
75 1 => format!(
76 "{kind} when creating directory {:?}",
77 paths.first().expect("match arm checks for a single entry")
78 ),
79 _many => format!("{kind} when creating directories {paths:?}"),
80 };
81
82 #[cfg(unix)]
83 {
84 if kind == io::ErrorKind::PermissionDenied {
85 error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\
86 \nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`");
87 }
88 }
89
90 Some(error_kind_details)
91 })
92 .collect::<Vec<_>>().join("\n\n");
93
94 eprintln!("{message}: {error_details}");
95 Application::new().run(move |cx| {
96 if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |_, cx| {
97 cx.new(|_| gpui::Empty)
98 }) {
99 window
100 .update(cx, |_, window, cx| {
101 let response = window.prompt(
102 gpui::PromptLevel::Critical,
103 message,
104 Some(&error_details),
105 &["Exit"],
106 cx,
107 );
108
109 cx.spawn_in(window, async move |_, cx| {
110 response.await?;
111 cx.update(|_, cx| cx.quit())
112 })
113 .detach_and_log_err(cx);
114 })
115 .log_err();
116 } else {
117 fail_to_open_window(anyhow::anyhow!("{message}: {error_details}"), cx)
118 }
119 })
120}
121
122fn fail_to_open_window_async(e: anyhow::Error, cx: &mut AsyncApp) {
123 cx.update(|cx| fail_to_open_window(e, cx)).log_err();
124}
125
126fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) {
127 eprintln!(
128 "Zed failed to open a window: {e:?}. See https://zed.dev/docs/linux for troubleshooting steps."
129 );
130 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
131 {
132 process::exit(1);
133 }
134
135 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
136 {
137 use ashpd::desktop::notification::{Notification, NotificationProxy, Priority};
138 _cx.spawn(async move |_cx| {
139 let Ok(proxy) = NotificationProxy::new().await else {
140 process::exit(1);
141 };
142
143 let notification_id = "dev.zed.Oops";
144 proxy
145 .add_notification(
146 notification_id,
147 Notification::new("Zed failed to launch")
148 .body(Some(
149 format!(
150 "{e:?}. See https://zed.dev/docs/linux for troubleshooting steps."
151 )
152 .as_str(),
153 ))
154 .priority(Priority::High)
155 .icon(ashpd::desktop::Icon::with_names(&[
156 "dialog-question-symbolic",
157 ])),
158 )
159 .await
160 .ok();
161
162 process::exit(1);
163 })
164 .detach();
165 }
166}
167
168pub fn main() {
169 #[cfg(unix)]
170 util::prevent_root_execution();
171
172 let args = Args::parse();
173
174 // `zed --crash-handler` Makes zed operate in minidump crash handler mode
175 if let Some(socket) = &args.crash_handler {
176 crashes::crash_server(socket.as_path());
177 return;
178 }
179
180 // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass
181 if let Some(socket) = &args.askpass {
182 askpass::main(socket);
183 return;
184 }
185
186 // `zed --nc` Makes zed operate in nc/netcat mode for use with MCP
187 if let Some(socket) = &args.nc {
188 match nc::main(socket) {
189 Ok(()) => return,
190 Err(err) => {
191 eprintln!("Error: {}", err);
192 process::exit(1);
193 }
194 }
195 }
196
197 // `zed --printenv` Outputs environment variables as JSON to stdout
198 if args.printenv {
199 util::shell_env::print_env();
200 return;
201 }
202
203 if args.dump_all_actions {
204 dump_all_gpui_actions();
205 return;
206 }
207
208 // Set custom data directory.
209 if let Some(dir) = &args.user_data_dir {
210 paths::set_custom_data_dir(dir);
211 }
212
213 #[cfg(all(not(debug_assertions), target_os = "windows"))]
214 unsafe {
215 use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole};
216
217 if args.foreground {
218 let _ = AttachConsole(ATTACH_PARENT_PROCESS);
219 }
220 }
221
222 let file_errors = init_paths();
223 if !file_errors.is_empty() {
224 files_not_created_on_launch(file_errors);
225 return;
226 }
227
228 zlog::init();
229 if stdout_is_a_pty() {
230 zlog::init_output_stdout();
231 } else {
232 let result = zlog::init_output_file(paths::log_file(), Some(paths::old_log_file()));
233 if let Err(err) = result {
234 eprintln!("Could not open log file: {}... Defaulting to stdout", err);
235 zlog::init_output_stdout();
236 };
237 }
238
239 let app_version = AppVersion::load(env!("CARGO_PKG_VERSION"));
240 let app_commit_sha =
241 option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string()));
242
243 if args.system_specs {
244 let system_specs = system_specs::SystemSpecs::new_stateless(
245 app_version,
246 app_commit_sha,
247 *release_channel::RELEASE_CHANNEL,
248 );
249 println!("Zed System Specs (from CLI):\n{}", system_specs);
250 return;
251 }
252
253 log::info!(
254 "========== starting zed version {}, sha {} ==========",
255 app_version,
256 app_commit_sha
257 .as_ref()
258 .map(|sha| sha.short())
259 .as_deref()
260 .unwrap_or("unknown"),
261 );
262
263 let app = Application::new().with_assets(Assets);
264
265 let system_id = app.background_executor().block(system_id()).ok();
266 let installation_id = app.background_executor().block(installation_id()).ok();
267 let session_id = Uuid::new_v4().to_string();
268 let session = app.background_executor().block(Session::new());
269
270 app.background_executor()
271 .spawn(crashes::init(InitCrashHandler {
272 session_id: session_id.clone(),
273 zed_version: app_version.to_string(),
274 release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(),
275 commit_sha: app_commit_sha
276 .as_ref()
277 .map(|sha| sha.full())
278 .unwrap_or_else(|| "no sha".to_owned()),
279 }))
280 .detach();
281 reliability::init_panic_hook(
282 app_version,
283 app_commit_sha.clone(),
284 system_id.as_ref().map(|id| id.to_string()),
285 installation_id.as_ref().map(|id| id.to_string()),
286 session_id.clone(),
287 );
288
289 let (open_listener, mut open_rx) = OpenListener::new();
290
291 let failed_single_instance_check = if *zed_env_vars::ZED_STATELESS
292 || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev
293 {
294 false
295 } else {
296 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
297 {
298 crate::zed::listen_for_cli_connections(open_listener.clone()).is_err()
299 }
300
301 #[cfg(target_os = "windows")]
302 {
303 !crate::zed::windows_only_instance::handle_single_instance(open_listener.clone(), &args)
304 }
305
306 #[cfg(target_os = "macos")]
307 {
308 use zed::mac_only_instance::*;
309 ensure_only_instance() != IsOnlyInstance::Yes
310 }
311 };
312 if failed_single_instance_check {
313 println!("zed is already running");
314 return;
315 }
316
317 let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
318 let git_binary_path =
319 if cfg!(target_os = "macos") && option_env!("ZED_BUNDLE").as_deref() == Some("true") {
320 app.path_for_auxiliary_executable("git")
321 .context("could not find git binary path")
322 .log_err()
323 } else {
324 None
325 };
326 log::info!("Using git binary path: {:?}", git_binary_path);
327
328 let fs = Arc::new(RealFs::new(git_binary_path, app.background_executor()));
329 let user_settings_file_rx = watch_config_file(
330 &app.background_executor(),
331 fs.clone(),
332 paths::settings_file().clone(),
333 );
334 let global_settings_file_rx = watch_config_file(
335 &app.background_executor(),
336 fs.clone(),
337 paths::global_settings_file().clone(),
338 );
339 let user_keymap_file_rx = watch_config_file(
340 &app.background_executor(),
341 fs.clone(),
342 paths::keymap_file().clone(),
343 );
344
345 let (shell_env_loaded_tx, shell_env_loaded_rx) = oneshot::channel();
346 if !stdout_is_a_pty() {
347 app.background_executor()
348 .spawn(async {
349 #[cfg(unix)]
350 util::load_login_shell_environment().log_err();
351 shell_env_loaded_tx.send(()).ok();
352 })
353 .detach()
354 } else {
355 drop(shell_env_loaded_tx)
356 }
357
358 app.on_open_urls({
359 let open_listener = open_listener.clone();
360 move |urls| {
361 open_listener.open(RawOpenRequest {
362 urls,
363 diff_paths: Vec::new(),
364 ..Default::default()
365 })
366 }
367 });
368 app.on_reopen(move |cx| {
369 if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade())
370 {
371 cx.spawn({
372 let app_state = app_state;
373 async move |cx| {
374 if let Err(e) = restore_or_create_workspace(app_state, cx).await {
375 fail_to_open_window_async(e, cx)
376 }
377 }
378 })
379 .detach();
380 }
381 });
382
383 app.run(move |cx| {
384 menu::init();
385 zed_actions::init();
386
387 release_channel::init(app_version, cx);
388 gpui_tokio::init(cx);
389 if let Some(app_commit_sha) = app_commit_sha {
390 AppCommitSha::set_global(app_commit_sha, cx);
391 }
392 settings::init(cx);
393 zlog_settings::init(cx);
394 handle_settings_file_changes(
395 user_settings_file_rx,
396 global_settings_file_rx,
397 cx,
398 handle_settings_changed,
399 );
400 handle_keymap_file_changes(user_keymap_file_rx, cx);
401 client::init_settings(cx);
402 let user_agent = format!(
403 "Zed/{} ({}; {})",
404 AppVersion::global(cx),
405 std::env::consts::OS,
406 std::env::consts::ARCH
407 );
408 let proxy_str = ProxySettings::get_global(cx).proxy.to_owned();
409 let proxy_url = proxy_str
410 .as_ref()
411 .and_then(|input| {
412 input
413 .parse::<Url>()
414 .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e))
415 .ok()
416 })
417 .or_else(read_proxy_from_env);
418 let http = {
419 let _guard = Tokio::handle(cx).enter();
420
421 ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent)
422 .expect("could not start HTTP client")
423 };
424 cx.set_http_client(Arc::new(http));
425
426 <dyn Fs>::set_global(fs.clone(), cx);
427
428 GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
429 git_hosting_providers::init(cx);
430
431 OpenListener::set_global(cx, open_listener.clone());
432
433 extension::init(cx);
434 let extension_host_proxy = ExtensionHostProxy::global(cx);
435
436 let client = Client::production(cx);
437 cx.set_http_client(client.http_client());
438 let mut languages = LanguageRegistry::new(cx.background_executor().clone());
439 languages.set_language_server_download_dir(paths::languages_dir().clone());
440 let languages = Arc::new(languages);
441 let (mut tx, rx) = watch::channel(None);
442 cx.observe_global::<SettingsStore>(move |cx| {
443 let settings = &ProjectSettings::get_global(cx).node;
444 let options = NodeBinaryOptions {
445 allow_path_lookup: !settings.ignore_system_version,
446 // TODO: Expose this setting
447 allow_binary_download: true,
448 use_paths: settings.path.as_ref().map(|node_path| {
449 let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref());
450 let npm_path = settings
451 .npm_path
452 .as_ref()
453 .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref()));
454 (
455 node_path.clone(),
456 npm_path.unwrap_or_else(|| {
457 let base_path = PathBuf::new();
458 node_path.parent().unwrap_or(&base_path).join("npm")
459 }),
460 )
461 }),
462 };
463 tx.send(Some(options)).log_err();
464 })
465 .detach();
466 let node_runtime = NodeRuntime::new(client.http_client(), Some(shell_env_loaded_rx), rx);
467
468 debug_adapter_extension::init(extension_host_proxy.clone(), cx);
469 language::init(cx);
470 languages::init(languages.clone(), node_runtime.clone(), cx);
471 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
472 let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
473
474 language_extension::init(
475 language_extension::LspAccess::ViaWorkspaces({
476 let workspace_store = workspace_store.clone();
477 Arc::new(move |cx: &mut App| {
478 workspace_store.update(cx, |workspace_store, cx| {
479 workspace_store
480 .workspaces()
481 .iter()
482 .map(|workspace| {
483 workspace.update(cx, |workspace, _, cx| {
484 workspace.project().read(cx).lsp_store()
485 })
486 })
487 .collect()
488 })
489 })
490 }),
491 extension_host_proxy.clone(),
492 languages.clone(),
493 );
494
495 Client::set_global(client.clone(), cx);
496
497 zed::init(cx);
498 project::Project::init(&client, cx);
499 debugger_ui::init(cx);
500 debugger_tools::init(cx);
501 client::init(&client, cx);
502 let telemetry = client.telemetry();
503 telemetry.start(
504 system_id.as_ref().map(|id| id.to_string()),
505 installation_id.as_ref().map(|id| id.to_string()),
506 session_id.clone(),
507 cx,
508 );
509
510 // We should rename these in the future to `first app open`, `first app open for release channel`, and `app open`
511 if let (Some(system_id), Some(installation_id)) = (&system_id, &installation_id) {
512 match (&system_id, &installation_id) {
513 (IdType::New(_), IdType::New(_)) => {
514 telemetry::event!("App First Opened");
515 telemetry::event!("App First Opened For Release Channel");
516 }
517 (IdType::Existing(_), IdType::New(_)) => {
518 telemetry::event!("App First Opened For Release Channel");
519 }
520 (_, IdType::Existing(_)) => {
521 telemetry::event!("App Opened");
522 }
523 }
524 }
525 let app_session = cx.new(|cx| AppSession::new(session, cx));
526
527 let app_state = Arc::new(AppState {
528 languages,
529 client: client.clone(),
530 user_store,
531 fs: fs.clone(),
532 build_window_options,
533 workspace_store,
534 node_runtime,
535 session: app_session,
536 });
537 AppState::set_global(Arc::downgrade(&app_state), cx);
538
539 auto_update::init(client.http_client(), cx);
540 dap_adapters::init(cx);
541 auto_update_ui::init(cx);
542 reliability::init(
543 client.http_client(),
544 system_id.as_ref().map(|id| id.to_string()),
545 installation_id.clone().map(|id| id.to_string()),
546 session_id.clone(),
547 cx,
548 );
549
550 SystemAppearance::init(cx);
551 theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
552 theme_extension::init(
553 extension_host_proxy.clone(),
554 ThemeRegistry::global(cx),
555 cx.background_executor().clone(),
556 );
557 command_palette::init(cx);
558 let copilot_language_server_id = app_state.languages.next_language_server_id();
559 copilot::init(
560 copilot_language_server_id,
561 app_state.fs.clone(),
562 app_state.client.http_client(),
563 app_state.node_runtime.clone(),
564 cx,
565 );
566 supermaven::init(app_state.client.clone(), cx);
567 language_model::init(app_state.client.clone(), cx);
568 language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
569 agent_settings::init(cx);
570 acp_tools::init(cx);
571 web_search::init(cx);
572 web_search_providers::init(app_state.client.clone(), cx);
573 snippet_provider::init(cx);
574 edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx);
575 let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx);
576 agent_ui::init(
577 app_state.fs.clone(),
578 app_state.client.clone(),
579 prompt_builder.clone(),
580 app_state.languages.clone(),
581 false,
582 cx,
583 );
584 assistant_tools::init(app_state.client.http_client(), cx);
585 repl::init(app_state.fs.clone(), cx);
586 extension_host::init(
587 extension_host_proxy,
588 app_state.fs.clone(),
589 app_state.client.clone(),
590 app_state.node_runtime.clone(),
591 cx,
592 );
593 recent_projects::init(cx);
594
595 load_embedded_fonts(cx);
596
597 app_state.languages.set_theme(cx.theme().clone());
598 editor::init(cx);
599 image_viewer::init(cx);
600 repl::notebook::init(cx);
601 diagnostics::init(cx);
602
603 audio::init(cx);
604 workspace::init(app_state.clone(), cx);
605 ui_prompt::init(cx);
606
607 go_to_line::init(cx);
608 file_finder::init(cx);
609 tab_switcher::init(cx);
610 outline::init(cx);
611 project_symbols::init(cx);
612 project_panel::init(cx);
613 outline_panel::init(cx);
614 tasks_ui::init(cx);
615 snippets_ui::init(cx);
616 channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
617 search::init(cx);
618 vim::init(cx);
619 terminal_view::init(cx);
620 journal::init(app_state.clone(), cx);
621 language_selector::init(cx);
622 line_ending_selector::init(cx);
623 toolchain_selector::init(cx);
624 theme_selector::init(cx);
625 settings_profile_selector::init(cx);
626 language_tools::init(cx);
627 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
628 notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
629 collab_ui::init(&app_state, cx);
630 git_ui::init(cx);
631 jj_ui::init(cx);
632 feedback::init(cx);
633 markdown_preview::init(cx);
634 svg_preview::init(cx);
635 onboarding::init(cx);
636 settings_ui::init(cx);
637 keymap_editor::init(cx);
638 extensions_ui::init(cx);
639 zeta::init(cx);
640 inspector_ui::init(app_state.clone(), cx);
641
642 cx.observe_global::<SettingsStore>({
643 let fs = fs.clone();
644 let languages = app_state.languages.clone();
645 let http = app_state.client.http_client();
646 let client = app_state.client.clone();
647 move |cx| {
648 for &mut window in cx.windows().iter_mut() {
649 let background_appearance = cx.theme().window_background_appearance();
650 window
651 .update(cx, |_, window, _| {
652 window.set_background_appearance(background_appearance)
653 })
654 .ok();
655 }
656
657 eager_load_active_theme_and_icon_theme(fs.clone(), cx);
658
659 languages.set_theme(cx.theme().clone());
660 let new_host = &client::ClientSettings::get_global(cx).server_url;
661 if &http.base_url() != new_host {
662 http.set_base_url(new_host);
663 if client.status().borrow().is_connected() {
664 client.reconnect(&cx.to_async());
665 }
666 }
667 }
668 })
669 .detach();
670 telemetry::event!(
671 "Settings Changed",
672 setting = "theme",
673 value = cx.theme().name.to_string()
674 );
675 telemetry::event!(
676 "Settings Changed",
677 setting = "keymap",
678 value = BaseKeymap::get_global(cx).to_string()
679 );
680 telemetry.flush_events().detach();
681
682 let fs = app_state.fs.clone();
683 load_user_themes_in_background(fs.clone(), cx);
684 watch_themes(fs.clone(), cx);
685 watch_languages(fs.clone(), app_state.languages.clone(), cx);
686
687 cx.set_menus(app_menus());
688 initialize_workspace(app_state.clone(), prompt_builder, cx);
689
690 cx.activate(true);
691
692 cx.spawn({
693 let client = app_state.client.clone();
694 async move |cx| authenticate(client, cx).await
695 })
696 .detach_and_log_err(cx);
697
698 let urls: Vec<_> = args
699 .paths_or_urls
700 .iter()
701 .map(|arg| parse_url_arg(arg, cx))
702 .collect();
703
704 let diff_paths: Vec<[String; 2]> = args
705 .diff
706 .chunks(2)
707 .map(|chunk| [chunk[0].clone(), chunk[1].clone()])
708 .collect();
709
710 #[cfg(target_os = "windows")]
711 let wsl = args.wsl;
712 #[cfg(not(target_os = "windows"))]
713 let wsl = None;
714
715 if !urls.is_empty() || !diff_paths.is_empty() {
716 open_listener.open(RawOpenRequest {
717 urls,
718 diff_paths,
719 wsl,
720 })
721 }
722
723 match open_rx
724 .try_next()
725 .ok()
726 .flatten()
727 .and_then(|request| OpenRequest::parse(request, cx).log_err())
728 {
729 Some(request) => {
730 handle_open_request(request, app_state.clone(), cx);
731 }
732 None => {
733 cx.spawn({
734 let app_state = app_state.clone();
735 async move |cx| {
736 if let Err(e) = restore_or_create_workspace(app_state, cx).await {
737 fail_to_open_window_async(e, cx)
738 }
739 }
740 })
741 .detach();
742 }
743 }
744
745 let app_state = app_state.clone();
746
747 crate::zed::component_preview::init(app_state.clone(), cx);
748
749 cx.spawn(async move |cx| {
750 while let Some(urls) = open_rx.next().await {
751 cx.update(|cx| {
752 if let Some(request) = OpenRequest::parse(urls, cx).log_err() {
753 handle_open_request(request, app_state.clone(), cx);
754 }
755 })
756 .ok();
757 }
758 })
759 .detach();
760 });
761}
762
763fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut App) {
764 if let Some(kind) = request.kind {
765 match kind {
766 OpenRequestKind::CliConnection(connection) => {
767 cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await)
768 .detach();
769 }
770 OpenRequestKind::Extension { extension_id } => {
771 cx.spawn(async move |cx| {
772 let workspace =
773 workspace::get_any_active_workspace(app_state, cx.clone()).await?;
774 workspace.update(cx, |_, window, cx| {
775 window.dispatch_action(
776 Box::new(zed_actions::Extensions {
777 category_filter: None,
778 id: Some(extension_id),
779 }),
780 cx,
781 );
782 })
783 })
784 .detach_and_log_err(cx);
785 }
786 OpenRequestKind::AgentPanel => {
787 cx.spawn(async move |cx| {
788 let workspace =
789 workspace::get_any_active_workspace(app_state, cx.clone()).await?;
790 workspace.update(cx, |workspace, window, cx| {
791 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
792 panel.focus_handle(cx).focus(window);
793 }
794 })
795 })
796 .detach_and_log_err(cx);
797 }
798 OpenRequestKind::DockMenuAction { index } => {
799 cx.perform_dock_menu_action(index);
800 }
801 }
802
803 return;
804 }
805
806 if let Some(connection_options) = request.remote_connection {
807 cx.spawn(async move |cx| {
808 let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect();
809 open_remote_project(
810 connection_options,
811 paths,
812 app_state,
813 workspace::OpenOptions::default(),
814 cx,
815 )
816 .await
817 })
818 .detach_and_log_err(cx);
819 return;
820 }
821
822 let mut task = None;
823 if !request.open_paths.is_empty() || !request.diff_paths.is_empty() {
824 let app_state = app_state.clone();
825 task = Some(cx.spawn(async move |cx| {
826 let paths_with_position =
827 derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
828 let (_window, results) = open_paths_with_positions(
829 &paths_with_position,
830 &request.diff_paths,
831 app_state,
832 workspace::OpenOptions::default(),
833 cx,
834 )
835 .await?;
836 for result in results.into_iter().flatten() {
837 if let Err(err) = result {
838 log::error!("Error opening path: {err}",);
839 }
840 }
841 anyhow::Ok(())
842 }));
843 }
844
845 if !request.open_channel_notes.is_empty() || request.join_channel.is_some() {
846 cx.spawn(async move |cx| {
847 let result = maybe!(async {
848 if let Some(task) = task {
849 task.await?;
850 }
851 let client = app_state.client.clone();
852 // we continue even if authentication fails as join_channel/ open channel notes will
853 // show a visible error message.
854 authenticate(client, cx).await.log_err();
855
856 if let Some(channel_id) = request.join_channel {
857 cx.update(|cx| {
858 workspace::join_channel(
859 client::ChannelId(channel_id),
860 app_state.clone(),
861 None,
862 cx,
863 )
864 })?
865 .await?;
866 }
867
868 let workspace_window =
869 workspace::get_any_active_workspace(app_state, cx.clone()).await?;
870 let workspace = workspace_window.entity(cx)?;
871
872 let mut promises = Vec::new();
873 for (channel_id, heading) in request.open_channel_notes {
874 promises.push(cx.update_window(workspace_window.into(), |_, window, cx| {
875 ChannelView::open(
876 client::ChannelId(channel_id),
877 heading,
878 workspace.clone(),
879 window,
880 cx,
881 )
882 .log_err()
883 })?)
884 }
885 future::join_all(promises).await;
886 anyhow::Ok(())
887 })
888 .await;
889 if let Err(err) = result {
890 fail_to_open_window_async(err, cx);
891 }
892 })
893 .detach()
894 } else if let Some(task) = task {
895 cx.spawn(async move |cx| {
896 if let Err(err) = task.await {
897 fail_to_open_window_async(err, cx);
898 }
899 })
900 .detach();
901 }
902}
903
904async fn authenticate(client: Arc<Client>, cx: &AsyncApp) -> Result<()> {
905 if stdout_is_a_pty() {
906 if client::IMPERSONATE_LOGIN.is_some() {
907 client.sign_in_with_optional_connect(false, cx).await?;
908 } else if client.has_credentials(cx).await {
909 client.sign_in_with_optional_connect(true, cx).await?;
910 }
911 } else if client.has_credentials(cx).await {
912 client.sign_in_with_optional_connect(true, cx).await?;
913 }
914
915 Ok(())
916}
917
918async fn system_id() -> Result<IdType> {
919 let key_name = "system_id".to_string();
920
921 if let Ok(Some(system_id)) = GLOBAL_KEY_VALUE_STORE.read_kvp(&key_name) {
922 return Ok(IdType::Existing(system_id));
923 }
924
925 let system_id = Uuid::new_v4().to_string();
926
927 GLOBAL_KEY_VALUE_STORE
928 .write_kvp(key_name, system_id.clone())
929 .await?;
930
931 Ok(IdType::New(system_id))
932}
933
934async fn installation_id() -> Result<IdType> {
935 let legacy_key_name = "device_id".to_string();
936 let key_name = "installation_id".to_string();
937
938 // Migrate legacy key to new key
939 if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&legacy_key_name) {
940 KEY_VALUE_STORE
941 .write_kvp(key_name, installation_id.clone())
942 .await?;
943 KEY_VALUE_STORE.delete_kvp(legacy_key_name).await?;
944 return Ok(IdType::Existing(installation_id));
945 }
946
947 if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&key_name) {
948 return Ok(IdType::Existing(installation_id));
949 }
950
951 let installation_id = Uuid::new_v4().to_string();
952
953 KEY_VALUE_STORE
954 .write_kvp(key_name, installation_id.clone())
955 .await?;
956
957 Ok(IdType::New(installation_id))
958}
959
960async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp) -> Result<()> {
961 if let Some(locations) = restorable_workspace_locations(cx, &app_state).await {
962 let use_system_window_tabs = cx
963 .update(|cx| WorkspaceSettings::get_global(cx).use_system_window_tabs)
964 .unwrap_or(false);
965 let mut results: Vec<Result<(), Error>> = Vec::new();
966 let mut tasks = Vec::new();
967
968 for (index, (location, paths)) in locations.into_iter().enumerate() {
969 match location {
970 SerializedWorkspaceLocation::Local => {
971 let app_state = app_state.clone();
972 let task = cx.spawn(async move |cx| {
973 let open_task = cx.update(|cx| {
974 workspace::open_paths(
975 &paths.paths(),
976 app_state,
977 workspace::OpenOptions::default(),
978 cx,
979 )
980 })?;
981 open_task.await.map(|_| ())
982 });
983
984 // If we're using system window tabs and this is the first workspace,
985 // wait for it to finish so that the other windows can be added as tabs.
986 if use_system_window_tabs && index == 0 {
987 results.push(task.await);
988 } else {
989 tasks.push(task);
990 }
991 }
992 SerializedWorkspaceLocation::Remote(mut connection_options) => {
993 let app_state = app_state.clone();
994 if let RemoteConnectionOptions::Ssh(options) = &mut connection_options {
995 cx.update(|cx| {
996 SshSettings::get_global(cx)
997 .fill_connection_options_from_settings(options)
998 })?;
999 }
1000 let task = cx.spawn(async move |cx| {
1001 recent_projects::open_remote_project(
1002 connection_options,
1003 paths.paths().into_iter().map(PathBuf::from).collect(),
1004 app_state,
1005 workspace::OpenOptions::default(),
1006 cx,
1007 )
1008 .await
1009 .map_err(|e| anyhow::anyhow!(e))
1010 });
1011 tasks.push(task);
1012 }
1013 }
1014 }
1015
1016 // Wait for all workspaces to open concurrently
1017 results.extend(future::join_all(tasks).await);
1018
1019 // Show notifications for any errors that occurred
1020 let mut error_count = 0;
1021 for result in results {
1022 if let Err(e) = result {
1023 log::error!("Failed to restore workspace: {}", e);
1024 error_count += 1;
1025 }
1026 }
1027
1028 if error_count > 0 {
1029 let message = if error_count == 1 {
1030 "Failed to restore 1 workspace. Check logs for details.".to_string()
1031 } else {
1032 format!(
1033 "Failed to restore {} workspaces. Check logs for details.",
1034 error_count
1035 )
1036 };
1037
1038 // Try to find an active workspace to show the toast
1039 let toast_shown = cx
1040 .update(|cx| {
1041 if let Some(window) = cx.active_window()
1042 && let Some(workspace) = window.downcast::<Workspace>()
1043 {
1044 workspace
1045 .update(cx, |workspace, _, cx| {
1046 workspace.show_toast(
1047 Toast::new(NotificationId::unique::<()>(), message),
1048 cx,
1049 )
1050 })
1051 .ok();
1052 return true;
1053 }
1054 false
1055 })
1056 .unwrap_or(false);
1057
1058 // If we couldn't show a toast (no windows opened successfully),
1059 // we've already logged the errors above, so the user can check logs
1060 if !toast_shown {
1061 log::error!(
1062 "Failed to show notification for window restoration errors, because no workspace windows were available."
1063 );
1064 }
1065 }
1066 } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
1067 cx.update(|cx| show_onboarding_view(app_state, cx))?.await?;
1068 } else {
1069 cx.update(|cx| {
1070 workspace::open_new(
1071 Default::default(),
1072 app_state,
1073 cx,
1074 |workspace, window, cx| {
1075 Editor::new_file(workspace, &Default::default(), window, cx)
1076 },
1077 )
1078 })?
1079 .await?;
1080 }
1081
1082 Ok(())
1083}
1084
1085pub(crate) async fn restorable_workspace_locations(
1086 cx: &mut AsyncApp,
1087 app_state: &Arc<AppState>,
1088) -> Option<Vec<(SerializedWorkspaceLocation, PathList)>> {
1089 let mut restore_behavior = cx
1090 .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup)
1091 .ok()?;
1092
1093 let session_handle = app_state.session.clone();
1094 let (last_session_id, last_session_window_stack) = cx
1095 .update(|cx| {
1096 let session = session_handle.read(cx);
1097
1098 (
1099 session.last_session_id().map(|id| id.to_string()),
1100 session.last_session_window_stack(),
1101 )
1102 })
1103 .ok()?;
1104
1105 if last_session_id.is_none()
1106 && matches!(
1107 restore_behavior,
1108 workspace::RestoreOnStartupBehavior::LastSession
1109 )
1110 {
1111 restore_behavior = workspace::RestoreOnStartupBehavior::LastWorkspace;
1112 }
1113
1114 match restore_behavior {
1115 workspace::RestoreOnStartupBehavior::LastWorkspace => {
1116 workspace::last_opened_workspace_location()
1117 .await
1118 .map(|location| vec![location])
1119 }
1120 workspace::RestoreOnStartupBehavior::LastSession => {
1121 if let Some(last_session_id) = last_session_id {
1122 let ordered = last_session_window_stack.is_some();
1123
1124 let mut locations = workspace::last_session_workspace_locations(
1125 &last_session_id,
1126 last_session_window_stack,
1127 )
1128 .filter(|locations| !locations.is_empty());
1129
1130 // Since last_session_window_order returns the windows ordered front-to-back
1131 // we need to open the window that was frontmost last.
1132 if ordered && let Some(locations) = locations.as_mut() {
1133 locations.reverse();
1134 }
1135
1136 locations
1137 } else {
1138 None
1139 }
1140 }
1141 _ => None,
1142 }
1143}
1144
1145fn init_paths() -> HashMap<io::ErrorKind, Vec<&'static Path>> {
1146 [
1147 paths::config_dir(),
1148 paths::extensions_dir(),
1149 paths::languages_dir(),
1150 paths::debug_adapters_dir(),
1151 paths::database_dir(),
1152 paths::logs_dir(),
1153 paths::temp_dir(),
1154 ]
1155 .into_iter()
1156 .fold(HashMap::default(), |mut errors, path| {
1157 if let Err(e) = std::fs::create_dir_all(path) {
1158 errors.entry(e.kind()).or_insert_with(Vec::new).push(path);
1159 }
1160 errors
1161 })
1162}
1163
1164pub fn stdout_is_a_pty() -> bool {
1165 std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal()
1166}
1167
1168#[derive(Parser, Debug)]
1169#[command(name = "zed", disable_version_flag = true)]
1170struct Args {
1171 /// A sequence of space-separated paths or urls that you want to open.
1172 ///
1173 /// Use `path:line:row` syntax to open a file at a specific location.
1174 /// Non-existing paths and directories will ignore `:line:row` suffix.
1175 ///
1176 /// URLs can either be `file://` or `zed://` scheme, or relative to <https://zed.dev>.
1177 paths_or_urls: Vec<String>,
1178
1179 /// Pairs of file paths to diff. Can be specified multiple times.
1180 #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
1181 diff: Vec<String>,
1182
1183 /// Sets a custom directory for all user data (e.g., database, extensions, logs).
1184 /// This overrides the default platform-specific data directory location.
1185 /// On macOS, the default is `~/Library/Application Support/Zed`.
1186 /// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`.
1187 /// On Windows, the default is `%LOCALAPPDATA%\Zed`.
1188 #[arg(long, value_name = "DIR")]
1189 user_data_dir: Option<String>,
1190
1191 /// The username and WSL distribution to use when opening paths. If not specified,
1192 /// Zed will attempt to open the paths directly.
1193 ///
1194 /// The username is optional, and if not specified, the default user for the distribution
1195 /// will be used.
1196 ///
1197 /// Example: `me@Ubuntu` or `Ubuntu`.
1198 ///
1199 /// WARN: You should not fill in this field by hand.
1200 #[cfg(target_os = "windows")]
1201 #[arg(long, value_name = "USER@DISTRO")]
1202 wsl: Option<String>,
1203
1204 /// Instructs zed to run as a dev server on this machine. (not implemented)
1205 #[arg(long)]
1206 dev_server_token: Option<String>,
1207
1208 /// Prints system specs. Useful for submitting issues on GitHub when encountering a bug
1209 /// that prevents Zed from starting, so you can't run `zed: copy system specs to clipboard`
1210 #[arg(long)]
1211 system_specs: bool,
1212
1213 /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency,
1214 /// by having Zed act like netcat communicating over a Unix socket.
1215 #[arg(long, hide = true)]
1216 askpass: Option<String>,
1217
1218 /// Used for the MCP Server, to remove the need for netcat as a dependency,
1219 /// by having Zed act like netcat communicating over a Unix socket.
1220 #[arg(long, hide = true)]
1221 nc: Option<String>,
1222
1223 /// Used for recording minidumps on crashes by having Zed run a separate
1224 /// process communicating over a socket.
1225 #[arg(long, hide = true)]
1226 crash_handler: Option<PathBuf>,
1227
1228 /// Run zed in the foreground, only used on Windows, to match the behavior on macOS.
1229 #[arg(long)]
1230 #[cfg(target_os = "windows")]
1231 #[arg(hide = true)]
1232 foreground: bool,
1233
1234 /// The dock action to perform. This is used on Windows only.
1235 #[arg(long)]
1236 #[cfg(target_os = "windows")]
1237 #[arg(hide = true)]
1238 dock_action: Option<usize>,
1239
1240 #[arg(long, hide = true)]
1241 dump_all_actions: bool,
1242
1243 /// Output current environment variables as JSON to stdout
1244 #[arg(long, hide = true)]
1245 printenv: bool,
1246}
1247
1248#[derive(Clone, Debug)]
1249enum IdType {
1250 New(String),
1251 Existing(String),
1252}
1253
1254impl ToString for IdType {
1255 fn to_string(&self) -> String {
1256 match self {
1257 IdType::New(id) | IdType::Existing(id) => id.clone(),
1258 }
1259 }
1260}
1261
1262fn parse_url_arg(arg: &str, cx: &App) -> String {
1263 match std::fs::canonicalize(Path::new(&arg)) {
1264 Ok(path) => format!("file://{}", path.display()),
1265 Err(_) => {
1266 if arg.starts_with("file://")
1267 || arg.starts_with("zed-cli://")
1268 || arg.starts_with("ssh://")
1269 || parse_zed_link(arg, cx).is_some()
1270 {
1271 arg.into()
1272 } else {
1273 format!("file://{arg}")
1274 }
1275 }
1276 }
1277}
1278
1279fn load_embedded_fonts(cx: &App) {
1280 let asset_source = cx.asset_source();
1281 let font_paths = asset_source.list("fonts").unwrap();
1282 let embedded_fonts = Mutex::new(Vec::new());
1283 let executor = cx.background_executor();
1284
1285 executor.block(executor.scoped(|scope| {
1286 for font_path in &font_paths {
1287 if !font_path.ends_with(".ttf") {
1288 continue;
1289 }
1290
1291 scope.spawn(async {
1292 let font_bytes = asset_source.load(font_path).unwrap().unwrap();
1293 embedded_fonts.lock().push(font_bytes);
1294 });
1295 }
1296 }));
1297
1298 cx.text_system()
1299 .add_fonts(embedded_fonts.into_inner())
1300 .unwrap();
1301}
1302
1303/// Eagerly loads the active theme and icon theme based on the selections in the
1304/// theme settings.
1305///
1306/// This fast path exists to load these themes as soon as possible so the user
1307/// doesn't see the default themes while waiting on extensions to load.
1308fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &App) {
1309 let extension_store = ExtensionStore::global(cx);
1310 let theme_registry = ThemeRegistry::global(cx);
1311 let theme_settings = ThemeSettings::get_global(cx);
1312 let appearance = SystemAppearance::global(cx).0;
1313
1314 if let Some(theme_selection) = theme_settings.theme_selection.as_ref() {
1315 let theme_name = theme_selection.theme(appearance);
1316 if matches!(theme_registry.get(theme_name), Err(ThemeNotFoundError(_)))
1317 && let Some(theme_path) = extension_store.read(cx).path_to_extension_theme(theme_name)
1318 {
1319 cx.spawn({
1320 let theme_registry = theme_registry.clone();
1321 let fs = fs.clone();
1322 async move |cx| {
1323 theme_registry.load_user_theme(&theme_path, fs).await?;
1324
1325 cx.update(|cx| {
1326 ThemeSettings::reload_current_theme(cx);
1327 })
1328 }
1329 })
1330 .detach_and_log_err(cx);
1331 }
1332 }
1333
1334 if let Some(icon_theme_selection) = theme_settings.icon_theme_selection.as_ref() {
1335 let icon_theme_name = icon_theme_selection.icon_theme(appearance);
1336 if matches!(
1337 theme_registry.get_icon_theme(icon_theme_name),
1338 Err(IconThemeNotFoundError(_))
1339 ) && let Some((icon_theme_path, icons_root_path)) = extension_store
1340 .read(cx)
1341 .path_to_extension_icon_theme(icon_theme_name)
1342 {
1343 cx.spawn({
1344 let fs = fs.clone();
1345 async move |cx| {
1346 theme_registry
1347 .load_icon_theme(&icon_theme_path, &icons_root_path, fs)
1348 .await?;
1349
1350 cx.update(|cx| {
1351 ThemeSettings::reload_current_icon_theme(cx);
1352 })
1353 }
1354 })
1355 .detach_and_log_err(cx);
1356 }
1357 }
1358}
1359
1360/// Spawns a background task to load the user themes from the themes directory.
1361fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut App) {
1362 cx.spawn({
1363 let fs = fs.clone();
1364 async move |cx| {
1365 if let Some(theme_registry) = cx.update(|cx| ThemeRegistry::global(cx)).log_err() {
1366 let themes_dir = paths::themes_dir().as_ref();
1367 match fs
1368 .metadata(themes_dir)
1369 .await
1370 .ok()
1371 .flatten()
1372 .map(|m| m.is_dir)
1373 {
1374 Some(is_dir) => {
1375 anyhow::ensure!(is_dir, "Themes dir path {themes_dir:?} is not a directory")
1376 }
1377 None => {
1378 fs.create_dir(themes_dir).await.with_context(|| {
1379 format!("Failed to create themes dir at path {themes_dir:?}")
1380 })?;
1381 }
1382 }
1383 theme_registry.load_user_themes(themes_dir, fs).await?;
1384 cx.update(ThemeSettings::reload_current_theme)?;
1385 }
1386 anyhow::Ok(())
1387 }
1388 })
1389 .detach_and_log_err(cx);
1390}
1391
1392/// Spawns a background task to watch the themes directory for changes.
1393fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut App) {
1394 use std::time::Duration;
1395 cx.spawn(async move |cx| {
1396 let (mut events, _) = fs
1397 .watch(paths::themes_dir(), Duration::from_millis(100))
1398 .await;
1399
1400 while let Some(paths) = events.next().await {
1401 for event in paths {
1402 if fs.metadata(&event.path).await.ok().flatten().is_some()
1403 && let Some(theme_registry) =
1404 cx.update(|cx| ThemeRegistry::global(cx)).log_err()
1405 && let Some(()) = theme_registry
1406 .load_user_theme(&event.path, fs.clone())
1407 .await
1408 .log_err()
1409 {
1410 cx.update(ThemeSettings::reload_current_theme).log_err();
1411 }
1412 }
1413 }
1414 })
1415 .detach()
1416}
1417
1418#[cfg(debug_assertions)]
1419fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>, cx: &mut App) {
1420 use std::time::Duration;
1421
1422 cx.background_spawn(async move {
1423 let languages_src = Path::new("crates/languages/src");
1424 let Some(languages_src) = fs.canonicalize(languages_src).await.log_err() else {
1425 return;
1426 };
1427
1428 let (mut events, watcher) = fs.watch(&languages_src, Duration::from_millis(100)).await;
1429
1430 // add subdirectories since fs.watch is not recursive on Linux
1431 if let Some(mut paths) = fs.read_dir(&languages_src).await.log_err() {
1432 while let Some(path) = paths.next().await {
1433 if let Some(path) = path.log_err()
1434 && fs.is_dir(&path).await
1435 {
1436 watcher.add(&path).log_err();
1437 }
1438 }
1439 }
1440
1441 while let Some(event) = events.next().await {
1442 let has_language_file = event
1443 .iter()
1444 .any(|event| event.path.extension().is_some_and(|ext| ext == "scm"));
1445 if has_language_file {
1446 languages.reload();
1447 }
1448 }
1449 })
1450 .detach();
1451}
1452
1453#[cfg(not(debug_assertions))]
1454fn watch_languages(_fs: Arc<dyn fs::Fs>, _languages: Arc<LanguageRegistry>, _cx: &mut App) {}
1455
1456fn dump_all_gpui_actions() {
1457 #[derive(Debug, serde::Serialize)]
1458 struct ActionDef {
1459 name: &'static str,
1460 human_name: String,
1461 aliases: &'static [&'static str],
1462 documentation: Option<&'static str>,
1463 }
1464 let mut actions = gpui::generate_list_of_all_registered_actions()
1465 .map(|action| ActionDef {
1466 name: action.name,
1467 human_name: command_palette::humanize_action_name(action.name),
1468 aliases: action.deprecated_aliases,
1469 documentation: action.documentation,
1470 })
1471 .collect::<Vec<ActionDef>>();
1472
1473 actions.sort_by_key(|a| a.name);
1474
1475 io::Write::write(
1476 &mut std::io::stdout(),
1477 serde_json::to_string_pretty(&actions).unwrap().as_bytes(),
1478 )
1479 .unwrap();
1480}