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