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