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