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