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