1// Allow binary to be called Zed for a nice application menu when running executable direcly
2#![allow(non_snake_case)]
3
4use anyhow::{anyhow, Context, Result};
5use assets::Assets;
6use backtrace::Backtrace;
7use cli::{
8 ipc::{self, IpcSender},
9 CliRequest, CliResponse, IpcHandshake,
10};
11use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
12use db::kvp::KEY_VALUE_STORE;
13use futures::{
14 channel::{mpsc, oneshot},
15 FutureExt, SinkExt, StreamExt,
16};
17use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
18use isahc::{config::Configurable, Request};
19use language::LanguageRegistry;
20use log::LevelFilter;
21use node_runtime::NodeRuntime;
22use parking_lot::Mutex;
23use project::Fs;
24use serde_json::json;
25use settings::{
26 self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent,
27 WorkingDirectory,
28};
29use simplelog::ConfigBuilder;
30use smol::process::Command;
31use std::{
32 env, ffi::OsStr, fs::OpenOptions, io::Write as _, os::unix::prelude::OsStrExt, panic,
33 path::PathBuf, sync::Arc, thread, time::Duration,
34};
35use terminal_view::{get_working_directory, TerminalView};
36use util::http::{self, HttpClient};
37use welcome::{show_welcome_experience, FIRST_OPEN};
38
39use fs::RealFs;
40use settings::watched_json::WatchedJsonFile;
41#[cfg(debug_assertions)]
42use staff_mode::StaffMode;
43use theme::ThemeRegistry;
44use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
45use workspace::{
46 self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
47 OpenPaths, Workspace,
48};
49use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
50
51fn main() {
52 let http = http::client();
53 init_paths();
54 init_logger();
55
56 log::info!("========== starting zed ==========");
57 let mut app = gpui::App::new(Assets).unwrap();
58
59 let app_version = ZED_APP_VERSION
60 .or_else(|| app.platform().app_version().ok())
61 .map_or("dev".to_string(), |v| v.to_string());
62 init_panic_hook(app_version);
63
64 app.background();
65
66 load_embedded_fonts(&app);
67
68 let fs = Arc::new(RealFs);
69
70 let themes = ThemeRegistry::new(Assets, app.font_cache());
71 let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes);
72 let config_files = load_config_files(&app, fs.clone());
73
74 let login_shell_env_loaded = if stdout_is_a_pty() {
75 Task::ready(())
76 } else {
77 app.background().spawn(async {
78 load_login_shell_environment().await.log_err();
79 })
80 };
81
82 let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded();
83 let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded();
84 app.on_open_urls(move |urls, _| {
85 if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
86 if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
87 cli_connections_tx
88 .unbounded_send(cli_connection)
89 .map_err(|_| anyhow!("no listener for cli connections"))
90 .log_err();
91 };
92 } else {
93 let paths: Vec<_> = urls
94 .iter()
95 .flat_map(|url| url.strip_prefix("file://"))
96 .map(|url| {
97 let decoded = urlencoding::decode_binary(url.as_bytes());
98 PathBuf::from(OsStr::from_bytes(decoded.as_ref()))
99 })
100 .collect();
101 open_paths_tx
102 .unbounded_send(paths)
103 .map_err(|_| anyhow!("no listener for open urls requests"))
104 .log_err();
105 }
106 });
107
108 app.run(move |cx| {
109 cx.set_global(*RELEASE_CHANNEL);
110
111 #[cfg(debug_assertions)]
112 cx.set_global(StaffMode(true));
113
114 let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap();
115
116 //Setup settings global before binding actions
117 cx.set_global(SettingsFile::new(
118 &paths::SETTINGS,
119 settings_file_content.clone(),
120 fs.clone(),
121 ));
122
123 settings::watch_files(
124 default_settings,
125 settings_file_content,
126 themes.clone(),
127 keymap_file,
128 cx,
129 );
130
131 if !stdout_is_a_pty() {
132 upload_previous_panics(http.clone(), cx);
133 }
134
135 let client = client::Client::new(http.clone(), cx);
136 let mut languages = LanguageRegistry::new(login_shell_env_loaded);
137 languages.set_executor(cx.background().clone());
138 languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
139 let languages = Arc::new(languages);
140 let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
141
142 languages::init(languages.clone(), themes.clone(), node_runtime.clone());
143 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
144
145 cx.set_global(client.clone());
146
147 context_menu::init(cx);
148 project::Project::init(&client);
149 client::init(client.clone(), cx);
150 command_palette::init(cx);
151 editor::init(cx);
152 go_to_line::init(cx);
153 file_finder::init(cx);
154 outline::init(cx);
155 project_symbols::init(cx);
156 project_panel::init(cx);
157 diagnostics::init(cx);
158 search::init(cx);
159 vim::init(cx);
160 terminal_view::init(cx);
161 theme_testbench::init(cx);
162 recent_projects::init(cx);
163 copilot::init(client.clone(), node_runtime, cx);
164
165 cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
166 .detach();
167
168 languages.set_theme(cx.global::<Settings>().theme.clone());
169 cx.observe_global::<Settings, _>({
170 let languages = languages.clone();
171 move |cx| languages.set_theme(cx.global::<Settings>().theme.clone())
172 })
173 .detach();
174
175 client.start_telemetry();
176 client.report_event(
177 "start app",
178 Default::default(),
179 cx.global::<Settings>().telemetry(),
180 );
181
182 let app_state = Arc::new(AppState {
183 languages,
184 themes,
185 client: client.clone(),
186 user_store,
187 fs,
188 build_window_options,
189 initialize_workspace,
190 dock_default_item_factory,
191 background_actions,
192 });
193 auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
194
195 workspace::init(app_state.clone(), cx);
196
197 journal::init(app_state.clone(), cx);
198 language_selector::init(app_state.clone(), cx);
199 theme_selector::init(app_state.clone(), cx);
200 zed::init(&app_state, cx);
201 collab_ui::init(app_state.clone(), cx);
202 feedback::init(app_state.clone(), cx);
203 welcome::init(cx);
204
205 cx.set_menus(menus::menus());
206
207 if stdout_is_a_pty() {
208 cx.platform().activate(true);
209 let paths = collect_path_args();
210 if paths.is_empty() {
211 cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await })
212 .detach()
213 } else {
214 cx.dispatch_global_action(OpenPaths { paths });
215 }
216 } else {
217 if let Ok(Some(connection)) = cli_connections_rx.try_next() {
218 cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
219 .detach();
220 } else if let Ok(Some(paths)) = open_paths_rx.try_next() {
221 cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
222 .detach();
223 } else {
224 cx.spawn({
225 let app_state = app_state.clone();
226 |cx| async move { restore_or_create_workspace(&app_state, cx).await }
227 })
228 .detach()
229 }
230
231 cx.spawn(|cx| {
232 let app_state = app_state.clone();
233 async move {
234 while let Some(connection) = cli_connections_rx.next().await {
235 handle_cli_connection(connection, app_state.clone(), cx.clone()).await;
236 }
237 }
238 })
239 .detach();
240
241 cx.spawn(|mut cx| {
242 let app_state = app_state.clone();
243 async move {
244 while let Some(paths) = open_paths_rx.next().await {
245 cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
246 .detach();
247 }
248 }
249 })
250 .detach();
251 }
252
253 cx.spawn(|cx| async move {
254 if stdout_is_a_pty() {
255 if client::IMPERSONATE_LOGIN.is_some() {
256 client.authenticate_and_connect(false, &cx).await?;
257 }
258 } else if client.has_keychain_credentials(&cx) {
259 client.authenticate_and_connect(true, &cx).await?;
260 }
261 Ok::<_, anyhow::Error>(())
262 })
263 .detach_and_log_err(cx);
264 });
265}
266
267async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
268 if let Some(location) = workspace::last_opened_workspace_paths().await {
269 cx.update(|cx| {
270 cx.dispatch_global_action(OpenPaths {
271 paths: location.paths().as_ref().clone(),
272 })
273 });
274 } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
275 cx.update(|cx| show_welcome_experience(app_state, cx));
276 } else {
277 cx.update(|cx| {
278 cx.dispatch_global_action(NewFile);
279 });
280 }
281}
282
283fn init_paths() {
284 std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path");
285 std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path");
286 std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path");
287 std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path");
288}
289
290fn init_logger() {
291 if stdout_is_a_pty() {
292 env_logger::init();
293 } else {
294 let level = LevelFilter::Info;
295
296 // Prevent log file from becoming too large.
297 const KIB: u64 = 1024;
298 const MIB: u64 = 1024 * KIB;
299 const MAX_LOG_BYTES: u64 = MIB;
300 if std::fs::metadata(&*paths::LOG).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES)
301 {
302 let _ = std::fs::rename(&*paths::LOG, &*paths::OLD_LOG);
303 }
304
305 let log_file = OpenOptions::new()
306 .create(true)
307 .append(true)
308 .open(&*paths::LOG)
309 .expect("could not open logfile");
310
311 let config = ConfigBuilder::new()
312 .set_time_format_str("%Y-%m-%dT%T") //All timestamps are UTC
313 .build();
314
315 simplelog::WriteLogger::init(level, config, log_file).expect("could not initialize logger");
316 }
317}
318
319fn init_panic_hook(app_version: String) {
320 let is_pty = stdout_is_a_pty();
321 panic::set_hook(Box::new(move |info| {
322 let backtrace = Backtrace::new();
323
324 let thread = thread::current();
325 let thread = thread.name().unwrap_or("<unnamed>");
326
327 let payload = match info.payload().downcast_ref::<&'static str>() {
328 Some(s) => *s,
329 None => match info.payload().downcast_ref::<String>() {
330 Some(s) => &**s,
331 None => "Box<Any>",
332 },
333 };
334
335 let message = match info.location() {
336 Some(location) => {
337 format!(
338 "thread '{}' panicked at '{}': {}:{}{:?}",
339 thread,
340 payload,
341 location.file(),
342 location.line(),
343 backtrace
344 )
345 }
346 None => format!(
347 "thread '{}' panicked at '{}'{:?}",
348 thread, payload, backtrace
349 ),
350 };
351
352 if is_pty {
353 eprintln!("{}", message);
354 return;
355 }
356
357 let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
358 let panic_file_path =
359 paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, timestamp));
360 let panic_file = std::fs::OpenOptions::new()
361 .append(true)
362 .create(true)
363 .open(&panic_file_path)
364 .log_err();
365 if let Some(mut panic_file) = panic_file {
366 write!(&mut panic_file, "{}", message).log_err();
367 panic_file.flush().log_err();
368 }
369 }));
370}
371
372fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut MutableAppContext) {
373 let diagnostics_telemetry = cx.global::<Settings>().telemetry_diagnostics();
374
375 cx.background()
376 .spawn({
377 async move {
378 let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
379 let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?;
380 while let Some(child) = children.next().await {
381 let child = child?;
382 let child_path = child.path();
383
384 if child_path.extension() != Some(OsStr::new("panic")) {
385 continue;
386 }
387 let filename = if let Some(filename) = child_path.file_name() {
388 filename.to_string_lossy()
389 } else {
390 continue;
391 };
392
393 let mut components = filename.split('-');
394 if components.next() != Some("zed") {
395 continue;
396 }
397 let version = if let Some(version) = components.next() {
398 version
399 } else {
400 continue;
401 };
402
403 if diagnostics_telemetry {
404 let text = smol::fs::read_to_string(&child_path)
405 .await
406 .context("error reading panic file")?;
407 let body = serde_json::to_string(&json!({
408 "text": text,
409 "version": version,
410 "token": ZED_SECRET_CLIENT_TOKEN,
411 }))
412 .unwrap();
413 let request = Request::post(&panic_report_url)
414 .redirect_policy(isahc::config::RedirectPolicy::Follow)
415 .header("Content-Type", "application/json")
416 .body(body.into())?;
417 let response = http.send(request).await.context("error sending panic")?;
418 if !response.status().is_success() {
419 log::error!("Error uploading panic to server: {}", response.status());
420 }
421 }
422
423 // We've done what we can, delete the file
424 std::fs::remove_file(child_path)
425 .context("error removing panic")
426 .log_err();
427 }
428 Ok::<_, anyhow::Error>(())
429 }
430 .log_err()
431 })
432 .detach();
433}
434
435async fn load_login_shell_environment() -> Result<()> {
436 let marker = "ZED_LOGIN_SHELL_START";
437 let shell = env::var("SHELL").context(
438 "SHELL environment variable is not assigned so we can't source login environment variables",
439 )?;
440 let output = Command::new(&shell)
441 .args(["-lic", &format!("echo {marker} && /usr/bin/env -0")])
442 .output()
443 .await
444 .context("failed to spawn login shell to source login environment variables")?;
445 if !output.status.success() {
446 Err(anyhow!("login shell exited with error"))?;
447 }
448
449 let stdout = String::from_utf8_lossy(&output.stdout);
450
451 if let Some(env_output_start) = stdout.find(marker) {
452 let env_output = &stdout[env_output_start + marker.len()..];
453 for line in env_output.split_terminator('\0') {
454 if let Some(separator_index) = line.find('=') {
455 let key = &line[..separator_index];
456 let value = &line[separator_index + 1..];
457 env::set_var(key, value);
458 }
459 }
460 log::info!(
461 "set environment variables from shell:{}, path:{}",
462 shell,
463 env::var("PATH").unwrap_or_default(),
464 );
465 }
466
467 Ok(())
468}
469
470fn stdout_is_a_pty() -> bool {
471 unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 }
472}
473
474fn collect_path_args() -> Vec<PathBuf> {
475 env::args()
476 .skip(1)
477 .filter_map(|arg| match std::fs::canonicalize(arg) {
478 Ok(path) => Some(path),
479 Err(error) => {
480 log::error!("error parsing path argument: {}", error);
481 None
482 }
483 })
484 .collect::<Vec<_>>()
485}
486
487fn load_embedded_fonts(app: &App) {
488 let font_paths = Assets.list("fonts");
489 let embedded_fonts = Mutex::new(Vec::new());
490 smol::block_on(app.background().scoped(|scope| {
491 for font_path in &font_paths {
492 scope.spawn(async {
493 let font_path = &*font_path;
494 let font_bytes = Assets.load(font_path).unwrap().to_vec();
495 embedded_fonts.lock().push(Arc::from(font_bytes));
496 });
497 }
498 }));
499 app.platform()
500 .fonts()
501 .add_fonts(&embedded_fonts.into_inner())
502 .unwrap();
503}
504
505#[cfg(debug_assertions)]
506async fn watch_themes(
507 fs: Arc<dyn Fs>,
508 themes: Arc<ThemeRegistry>,
509 mut cx: AsyncAppContext,
510) -> Option<()> {
511 let mut events = fs
512 .watch("styles/src".as_ref(), Duration::from_millis(100))
513 .await;
514 while (events.next().await).is_some() {
515 let output = Command::new("npm")
516 .current_dir("styles")
517 .args(["run", "build"])
518 .output()
519 .await
520 .log_err()?;
521 if output.status.success() {
522 cx.update(|cx| theme_selector::ThemeSelector::reload(themes.clone(), cx))
523 } else {
524 eprintln!(
525 "build script failed {}",
526 String::from_utf8_lossy(&output.stderr)
527 );
528 }
529 }
530 Some(())
531}
532
533#[cfg(not(debug_assertions))]
534async fn watch_themes(
535 _fs: Arc<dyn Fs>,
536 _themes: Arc<ThemeRegistry>,
537 _cx: AsyncAppContext,
538) -> Option<()> {
539 None
540}
541
542fn load_config_files(
543 app: &App,
544 fs: Arc<dyn Fs>,
545) -> oneshot::Receiver<(
546 WatchedJsonFile<SettingsFileContent>,
547 WatchedJsonFile<KeymapFileContent>,
548)> {
549 let executor = app.background();
550 let (tx, rx) = oneshot::channel();
551 executor
552 .clone()
553 .spawn(async move {
554 let settings_file =
555 WatchedJsonFile::new(fs.clone(), &executor, paths::SETTINGS.clone()).await;
556 let keymap_file = WatchedJsonFile::new(fs, &executor, paths::KEYMAP.clone()).await;
557 tx.send((settings_file, keymap_file)).ok()
558 })
559 .detach();
560 rx
561}
562
563fn connect_to_cli(
564 server_name: &str,
565) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
566 let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
567 .context("error connecting to cli")?;
568 let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
569 let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
570
571 handshake_tx
572 .send(IpcHandshake {
573 requests: request_tx,
574 responses: response_rx,
575 })
576 .context("error sending ipc handshake")?;
577
578 let (mut async_request_tx, async_request_rx) =
579 futures::channel::mpsc::channel::<CliRequest>(16);
580 thread::spawn(move || {
581 while let Ok(cli_request) = request_rx.recv() {
582 if smol::block_on(async_request_tx.send(cli_request)).is_err() {
583 break;
584 }
585 }
586 Ok::<_, anyhow::Error>(())
587 });
588
589 Ok((async_request_rx, response_tx))
590}
591
592async fn handle_cli_connection(
593 (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
594 app_state: Arc<AppState>,
595 mut cx: AsyncAppContext,
596) {
597 if let Some(request) = requests.next().await {
598 match request {
599 CliRequest::Open { paths, wait } => {
600 let paths = if paths.is_empty() {
601 workspace::last_opened_workspace_paths()
602 .await
603 .map(|location| location.paths().to_vec())
604 .unwrap_or(paths)
605 } else {
606 paths
607 };
608 let (workspace, items) = cx
609 .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
610 .await;
611
612 let mut errored = false;
613 let mut item_release_futures = Vec::new();
614 cx.update(|cx| {
615 for (item, path) in items.into_iter().zip(&paths) {
616 match item {
617 Some(Ok(item)) => {
618 let released = oneshot::channel();
619 item.on_release(
620 cx,
621 Box::new(move |_| {
622 let _ = released.0.send(());
623 }),
624 )
625 .detach();
626 item_release_futures.push(released.1);
627 }
628 Some(Err(err)) => {
629 responses
630 .send(CliResponse::Stderr {
631 message: format!("error opening {:?}: {}", path, err),
632 })
633 .log_err();
634 errored = true;
635 }
636 None => {}
637 }
638 }
639 });
640
641 if wait {
642 let background = cx.background();
643 let wait = async move {
644 if paths.is_empty() {
645 let (done_tx, done_rx) = oneshot::channel();
646 let _subscription = cx.update(|cx| {
647 cx.observe_release(&workspace, move |_, _| {
648 let _ = done_tx.send(());
649 })
650 });
651 drop(workspace);
652 let _ = done_rx.await;
653 } else {
654 let _ = futures::future::try_join_all(item_release_futures).await;
655 };
656 }
657 .fuse();
658 futures::pin_mut!(wait);
659
660 loop {
661 // Repeatedly check if CLI is still open to avoid wasting resources
662 // waiting for files or workspaces to close.
663 let mut timer = background.timer(Duration::from_secs(1)).fuse();
664 futures::select_biased! {
665 _ = wait => break,
666 _ = timer => {
667 if responses.send(CliResponse::Ping).is_err() {
668 break;
669 }
670 }
671 }
672 }
673 }
674
675 responses
676 .send(CliResponse::Exit {
677 status: i32::from(errored),
678 })
679 .log_err();
680 }
681 }
682 }
683}
684
685pub fn dock_default_item_factory(
686 workspace: &mut Workspace,
687 cx: &mut ViewContext<Workspace>,
688) -> Option<Box<dyn ItemHandle>> {
689 let strategy = cx
690 .global::<Settings>()
691 .terminal_overrides
692 .working_directory
693 .clone()
694 .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
695
696 let working_directory = get_working_directory(workspace, cx, strategy);
697
698 let window_id = cx.window_id();
699 let terminal = workspace
700 .project()
701 .update(cx, |project, cx| {
702 project.create_terminal(working_directory, window_id, cx)
703 })
704 .notify_err(workspace, cx)?;
705
706 let terminal_view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
707
708 Some(Box::new(terminal_view))
709}
710
711pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
712 &[
713 ("Go to file", &file_finder::Toggle),
714 ("Open command palette", &command_palette::Toggle),
715 ("Focus the dock", &FocusDock),
716 ("Open recent projects", &recent_projects::OpenRecent),
717 ("Change your settings", &OpenSettings),
718 ]
719}