main.rs

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