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}