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