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 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}