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