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