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}