1// Allow binary to be called Zed for a nice application menu when running executable directly
2#![allow(non_snake_case)]
3
4use anyhow::{anyhow, Context, Result};
5use backtrace::Backtrace;
6use cli::{
7 ipc::{self, IpcSender},
8 CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
9};
10use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
11use db::kvp::KEY_VALUE_STORE;
12use editor::{scroll::autoscroll::Autoscroll, Editor};
13use futures::{
14 channel::{mpsc, oneshot},
15 FutureExt, SinkExt, StreamExt,
16};
17use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task, ViewContext};
18use isahc::{config::Configurable, Request};
19use language::{LanguageRegistry, Point};
20use log::LevelFilter;
21use node_runtime::NodeRuntime;
22use parking_lot::Mutex;
23use project::Fs;
24use serde::{Deserialize, Serialize};
25use settings::{default_settings, handle_settings_file_changes, watch_config_file, SettingsStore};
26use simplelog::ConfigBuilder;
27use smol::process::Command;
28use std::{
29 collections::HashMap,
30 env,
31 ffi::OsStr,
32 fs::OpenOptions,
33 io::Write as _,
34 ops::Not,
35 os::unix::prelude::OsStrExt,
36 panic,
37 path::{Path, PathBuf},
38 str,
39 sync::{
40 atomic::{AtomicBool, Ordering},
41 Arc, Weak,
42 },
43 thread,
44 time::{Duration, SystemTime, UNIX_EPOCH},
45};
46use sum_tree::Bias;
47use terminal_view::{get_working_directory, TerminalSettings, TerminalView};
48use util::{
49 http::{self, HttpClient},
50 paths::PathLikeWithPosition,
51};
52use welcome::{show_welcome_experience, FIRST_OPEN};
53
54use fs::RealFs;
55#[cfg(debug_assertions)]
56use staff_mode::StaffMode;
57use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
58use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
59use zed::{
60 assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace,
61 languages, menus,
62};
63
64fn main() {
65 let http = http::client();
66 init_paths();
67 init_logger();
68
69 log::info!("========== starting zed ==========");
70 let mut app = gpui::App::new(Assets).unwrap();
71
72 init_panic_hook(&app);
73
74 app.background();
75
76 load_embedded_fonts(&app);
77
78 let fs = Arc::new(RealFs);
79 let user_settings_file_rx =
80 watch_config_file(app.background(), fs.clone(), paths::SETTINGS.clone());
81 let user_keymap_file_rx =
82 watch_config_file(app.background(), fs.clone(), paths::KEYMAP.clone());
83
84 let login_shell_env_loaded = if stdout_is_a_pty() {
85 Task::ready(())
86 } else {
87 app.background().spawn(async {
88 load_login_shell_environment().await.log_err();
89 })
90 };
91
92 let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded();
93 let cli_connections_tx = Arc::new(cli_connections_tx);
94 let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded();
95 let open_paths_tx = Arc::new(open_paths_tx);
96 let urls_callback_triggered = Arc::new(AtomicBool::new(false));
97
98 let callback_cli_connections_tx = Arc::clone(&cli_connections_tx);
99 let callback_open_paths_tx = Arc::clone(&open_paths_tx);
100 let callback_urls_callback_triggered = Arc::clone(&urls_callback_triggered);
101 app.on_open_urls(move |urls, _| {
102 callback_urls_callback_triggered.store(true, Ordering::Release);
103 open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx);
104 })
105 .on_reopen(move |cx| {
106 if cx.has_global::<Weak<AppState>>() {
107 if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
108 workspace::open_new(&app_state, cx, |workspace, cx| {
109 Editor::new_file(workspace, &Default::default(), cx)
110 })
111 .detach();
112 }
113 }
114 });
115
116 app.run(move |cx| {
117 cx.set_global(*RELEASE_CHANNEL);
118
119 #[cfg(debug_assertions)]
120 cx.set_global(StaffMode(true));
121
122 let mut store = SettingsStore::default();
123 store
124 .set_default_settings(default_settings().as_ref(), cx)
125 .unwrap();
126 cx.set_global(store);
127 handle_settings_file_changes(user_settings_file_rx, cx);
128 handle_keymap_file_changes(user_keymap_file_rx, cx);
129
130 let client = client::Client::new(http.clone(), cx);
131 let mut languages = LanguageRegistry::new(login_shell_env_loaded);
132 languages.set_executor(cx.background().clone());
133 languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
134 let languages = Arc::new(languages);
135 let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
136
137 languages::init(languages.clone(), node_runtime.clone());
138 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
139
140 cx.set_global(client.clone());
141
142 theme::init(Assets, cx);
143 context_menu::init(cx);
144 project::Project::init(&client, cx);
145 client::init(&client, cx);
146 command_palette::init(cx);
147 language::init(cx);
148 editor::init(cx);
149 go_to_line::init(cx);
150 file_finder::init(cx);
151 outline::init(cx);
152 project_symbols::init(cx);
153 project_panel::init(cx);
154 diagnostics::init(cx);
155 search::init(cx);
156 vim::init(cx);
157 terminal_view::init(cx);
158 theme_testbench::init(cx);
159 copilot::init(http.clone(), node_runtime, cx);
160 ai::init(cx);
161
162 cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
163 cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
164 .detach();
165
166 languages.set_theme(theme::current(cx).clone());
167 cx.observe_global::<SettingsStore, _>({
168 let languages = languages.clone();
169 move |cx| languages.set_theme(theme::current(cx).clone())
170 })
171 .detach();
172
173 client.telemetry().start();
174
175 let app_state = Arc::new(AppState {
176 languages,
177 client: client.clone(),
178 user_store,
179 fs,
180 build_window_options,
181 initialize_workspace,
182 background_actions,
183 });
184 cx.set_global(Arc::downgrade(&app_state));
185 auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
186
187 workspace::init(app_state.clone(), cx);
188 recent_projects::init(cx);
189
190 journal::init(app_state.clone(), cx);
191 language_selector::init(cx);
192 theme_selector::init(cx);
193 activity_indicator::init(cx);
194 lsp_log::init(cx);
195 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
196 collab_ui::init(&app_state, cx);
197 feedback::init(cx);
198 welcome::init(cx);
199 zed::init(&app_state, cx);
200
201 cx.set_menus(menus::menus());
202
203 if stdout_is_a_pty() {
204 cx.platform().activate(true);
205 let paths = collect_path_args();
206 if paths.is_empty() {
207 cx.spawn(|cx| async move { restore_or_create_workspace(&app_state, cx).await })
208 .detach()
209 } else {
210 workspace::open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx);
211 }
212 } else {
213 upload_previous_panics(http.clone(), cx);
214
215 // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead
216 // of an *app, hence gets no specific callbacks run. Emulate them here, if needed.
217 if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
218 && !urls_callback_triggered.load(Ordering::Acquire)
219 {
220 open_urls(collect_url_args(), &cli_connections_tx, &open_paths_tx)
221 }
222
223 if let Ok(Some(connection)) = cli_connections_rx.try_next() {
224 cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
225 .detach();
226 } else if let Ok(Some(paths)) = open_paths_rx.try_next() {
227 cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
228 .detach();
229 } else {
230 cx.spawn({
231 let app_state = app_state.clone();
232 |cx| async move { restore_or_create_workspace(&app_state, cx).await }
233 })
234 .detach()
235 }
236
237 cx.spawn(|cx| {
238 let app_state = app_state.clone();
239 async move {
240 while let Some(connection) = cli_connections_rx.next().await {
241 handle_cli_connection(connection, app_state.clone(), cx.clone()).await;
242 }
243 }
244 })
245 .detach();
246
247 cx.spawn(|mut cx| {
248 let app_state = app_state.clone();
249 async move {
250 while let Some(paths) = open_paths_rx.next().await {
251 cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
252 .detach();
253 }
254 }
255 })
256 .detach();
257 }
258
259 cx.spawn(|cx| async move {
260 if stdout_is_a_pty() {
261 if client::IMPERSONATE_LOGIN.is_some() {
262 client.authenticate_and_connect(false, &cx).await?;
263 }
264 } else if client.has_keychain_credentials(&cx) {
265 client.authenticate_and_connect(true, &cx).await?;
266 }
267 Ok::<_, anyhow::Error>(())
268 })
269 .detach_and_log_err(cx);
270 });
271}
272
273fn open_urls(
274 urls: Vec<String>,
275 cli_connections_tx: &mpsc::UnboundedSender<(
276 mpsc::Receiver<CliRequest>,
277 IpcSender<CliResponse>,
278 )>,
279 open_paths_tx: &mpsc::UnboundedSender<Vec<PathBuf>>,
280) {
281 if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) {
282 if let Some(cli_connection) = connect_to_cli(server_name).log_err() {
283 cli_connections_tx
284 .unbounded_send(cli_connection)
285 .map_err(|_| anyhow!("no listener for cli connections"))
286 .log_err();
287 };
288 } else {
289 let paths: Vec<_> = urls
290 .iter()
291 .flat_map(|url| url.strip_prefix("file://"))
292 .map(|url| {
293 let decoded = urlencoding::decode_binary(url.as_bytes());
294 PathBuf::from(OsStr::from_bytes(decoded.as_ref()))
295 })
296 .collect();
297 open_paths_tx
298 .unbounded_send(paths)
299 .map_err(|_| anyhow!("no listener for open urls requests"))
300 .log_err();
301 }
302}
303
304async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncAppContext) {
305 if let Some(location) = workspace::last_opened_workspace_paths().await {
306 cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))
307 .await
308 .log_err();
309 } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
310 cx.update(|cx| show_welcome_experience(app_state, cx));
311 } else {
312 cx.update(|cx| {
313 workspace::open_new(app_state, cx, |workspace, cx| {
314 Editor::new_file(workspace, &Default::default(), cx)
315 })
316 .detach();
317 });
318 }
319}
320
321fn init_paths() {
322 std::fs::create_dir_all(&*util::paths::CONFIG_DIR).expect("could not create config path");
323 std::fs::create_dir_all(&*util::paths::LANGUAGES_DIR).expect("could not create languages path");
324 std::fs::create_dir_all(&*util::paths::DB_DIR).expect("could not create database path");
325 std::fs::create_dir_all(&*util::paths::LOGS_DIR).expect("could not create logs path");
326}
327
328fn init_logger() {
329 if stdout_is_a_pty() {
330 env_logger::init();
331 } else {
332 let level = LevelFilter::Info;
333
334 // Prevent log file from becoming too large.
335 const KIB: u64 = 1024;
336 const MIB: u64 = 1024 * KIB;
337 const MAX_LOG_BYTES: u64 = MIB;
338 if std::fs::metadata(&*paths::LOG).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES)
339 {
340 let _ = std::fs::rename(&*paths::LOG, &*paths::OLD_LOG);
341 }
342
343 let log_file = OpenOptions::new()
344 .create(true)
345 .append(true)
346 .open(&*paths::LOG)
347 .expect("could not open logfile");
348
349 let config = ConfigBuilder::new()
350 .set_time_format_str("%Y-%m-%dT%T") //All timestamps are UTC
351 .build();
352
353 simplelog::WriteLogger::init(level, config, log_file).expect("could not initialize logger");
354 }
355}
356
357#[derive(Serialize, Deserialize)]
358struct LocationData {
359 file: String,
360 line: u32,
361}
362
363#[derive(Serialize, Deserialize)]
364struct Panic {
365 thread: String,
366 payload: String,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 location_data: Option<LocationData>,
369 backtrace: Vec<String>,
370 app_version: String,
371 release_channel: String,
372 os_name: String,
373 os_version: Option<String>,
374 architecture: String,
375 panicked_on: u128,
376 identifying_backtrace: Option<Vec<String>>,
377}
378
379#[derive(Serialize)]
380struct PanicRequest {
381 panic: Panic,
382 token: String,
383}
384
385fn init_panic_hook(app: &App) {
386 let is_pty = stdout_is_a_pty();
387 let platform = app.platform();
388
389 panic::set_hook(Box::new(move |info| {
390 let app_version = ZED_APP_VERSION
391 .or_else(|| platform.app_version().ok())
392 .map_or("dev".to_string(), |v| v.to_string());
393
394 let thread = thread::current();
395 let thread = thread.name().unwrap_or("<unnamed>");
396
397 let payload = info.payload();
398 let payload = None
399 .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string()))
400 .or_else(|| payload.downcast_ref::<String>().map(|s| s.clone()))
401 .unwrap_or_else(|| "Box<Any>".to_string());
402
403 let backtrace = Backtrace::new();
404 let backtrace = backtrace
405 .frames()
406 .iter()
407 .filter_map(|frame| {
408 let symbol = frame.symbols().first()?;
409 let path = symbol.filename()?;
410 Some((path, symbol.lineno(), format!("{:#}", symbol.name()?)))
411 })
412 .collect::<Vec<_>>();
413
414 let this_file_path = Path::new(file!());
415
416 // Find the first frame in the backtrace for this panic hook itself. Exclude
417 // that frame and all frames before it.
418 let mut start_frame_ix = 0;
419 let mut codebase_root_path = None;
420 for (ix, (path, _, _)) in backtrace.iter().enumerate() {
421 if path.ends_with(this_file_path) {
422 start_frame_ix = ix + 1;
423 codebase_root_path = path.ancestors().nth(this_file_path.components().count());
424 break;
425 }
426 }
427
428 // Exclude any subsequent frames inside of rust's panic handling system.
429 while let Some((path, _, _)) = backtrace.get(start_frame_ix) {
430 if path.starts_with("/rustc") {
431 start_frame_ix += 1;
432 } else {
433 break;
434 }
435 }
436
437 // Build two backtraces:
438 // * one for display, which includes symbol names for all frames, and files
439 // and line numbers for symbols in this codebase
440 // * one for identification and de-duplication, which only includes symbol
441 // names for symbols in this codebase.
442 let mut display_backtrace = Vec::new();
443 let mut identifying_backtrace = Vec::new();
444 for (path, line, symbol) in &backtrace[start_frame_ix..] {
445 display_backtrace.push(symbol.clone());
446
447 if let Some(codebase_root_path) = &codebase_root_path {
448 if let Ok(suffix) = path.strip_prefix(&codebase_root_path) {
449 identifying_backtrace.push(symbol.clone());
450
451 let display_path = suffix.to_string_lossy();
452 if let Some(line) = line {
453 display_backtrace.push(format!(" {display_path}:{line}"));
454 } else {
455 display_backtrace.push(format!(" {display_path}"));
456 }
457 }
458 }
459 }
460
461 let panic_data = Panic {
462 thread: thread.into(),
463 payload: payload.into(),
464 location_data: info.location().map(|location| LocationData {
465 file: location.file().into(),
466 line: location.line(),
467 }),
468 app_version: app_version.clone(),
469 release_channel: RELEASE_CHANNEL.dev_name().into(),
470 os_name: platform.os_name().into(),
471 os_version: platform
472 .os_version()
473 .ok()
474 .map(|os_version| os_version.to_string()),
475 architecture: env::consts::ARCH.into(),
476 panicked_on: SystemTime::now()
477 .duration_since(UNIX_EPOCH)
478 .unwrap()
479 .as_millis(),
480 backtrace: display_backtrace,
481 identifying_backtrace: identifying_backtrace
482 .is_empty()
483 .not()
484 .then_some(identifying_backtrace),
485 };
486
487 if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {
488 if is_pty {
489 eprintln!("{}", panic_data_json);
490 return;
491 }
492
493 let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
494 let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp));
495 let panic_file = std::fs::OpenOptions::new()
496 .append(true)
497 .create(true)
498 .open(&panic_file_path)
499 .log_err();
500 if let Some(mut panic_file) = panic_file {
501 write!(&mut panic_file, "{}", panic_data_json).log_err();
502 panic_file.flush().log_err();
503 }
504 }
505 }));
506}
507
508fn upload_previous_panics(http: Arc<dyn HttpClient>, cx: &mut AppContext) {
509 let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
510
511 cx.background()
512 .spawn({
513 async move {
514 let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
515 let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?;
516 while let Some(child) = children.next().await {
517 let child = child?;
518 let child_path = child.path();
519
520 if child_path.extension() != Some(OsStr::new("panic")) {
521 continue;
522 }
523 let filename = if let Some(filename) = child_path.file_name() {
524 filename.to_string_lossy()
525 } else {
526 continue;
527 };
528
529 if !filename.starts_with("zed") {
530 continue;
531 }
532
533 if telemetry_settings.diagnostics {
534 let panic_data_text = smol::fs::read_to_string(&child_path)
535 .await
536 .context("error reading panic file")?;
537
538 let body = serde_json::to_string(&PanicRequest {
539 panic: serde_json::from_str(&panic_data_text)?,
540 token: ZED_SECRET_CLIENT_TOKEN.into(),
541 })
542 .unwrap();
543
544 let request = Request::post(&panic_report_url)
545 .redirect_policy(isahc::config::RedirectPolicy::Follow)
546 .header("Content-Type", "application/json")
547 .body(body.into())?;
548 let response = http.send(request).await.context("error sending panic")?;
549 if !response.status().is_success() {
550 log::error!("Error uploading panic to server: {}", response.status());
551 }
552 }
553
554 // We've done what we can, delete the file
555 std::fs::remove_file(child_path)
556 .context("error removing panic")
557 .log_err();
558 }
559 Ok::<_, anyhow::Error>(())
560 }
561 .log_err()
562 })
563 .detach();
564}
565
566async fn load_login_shell_environment() -> Result<()> {
567 let marker = "ZED_LOGIN_SHELL_START";
568 let shell = env::var("SHELL").context(
569 "SHELL environment variable is not assigned so we can't source login environment variables",
570 )?;
571 let output = Command::new(&shell)
572 .args(["-lic", &format!("echo {marker} && /usr/bin/env -0")])
573 .output()
574 .await
575 .context("failed to spawn login shell to source login environment variables")?;
576 if !output.status.success() {
577 Err(anyhow!("login shell exited with error"))?;
578 }
579
580 let stdout = String::from_utf8_lossy(&output.stdout);
581
582 if let Some(env_output_start) = stdout.find(marker) {
583 let env_output = &stdout[env_output_start + marker.len()..];
584 for line in env_output.split_terminator('\0') {
585 if let Some(separator_index) = line.find('=') {
586 let key = &line[..separator_index];
587 let value = &line[separator_index + 1..];
588 env::set_var(key, value);
589 }
590 }
591 log::info!(
592 "set environment variables from shell:{}, path:{}",
593 shell,
594 env::var("PATH").unwrap_or_default(),
595 );
596 }
597
598 Ok(())
599}
600
601fn stdout_is_a_pty() -> bool {
602 std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none()
603 && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 }
604}
605
606fn collect_path_args() -> Vec<PathBuf> {
607 env::args()
608 .skip(1)
609 .filter_map(|arg| match std::fs::canonicalize(arg) {
610 Ok(path) => Some(path),
611 Err(error) => {
612 log::error!("error parsing path argument: {}", error);
613 None
614 }
615 })
616 .collect()
617}
618
619fn collect_url_args() -> Vec<String> {
620 env::args().skip(1).collect()
621}
622
623fn load_embedded_fonts(app: &App) {
624 let font_paths = Assets.list("fonts");
625 let embedded_fonts = Mutex::new(Vec::new());
626 smol::block_on(app.background().scoped(|scope| {
627 for font_path in &font_paths {
628 scope.spawn(async {
629 let font_path = &*font_path;
630 let font_bytes = Assets.load(font_path).unwrap().to_vec();
631 embedded_fonts.lock().push(Arc::from(font_bytes));
632 });
633 }
634 }));
635 app.platform()
636 .fonts()
637 .add_fonts(&embedded_fonts.into_inner())
638 .unwrap();
639}
640
641#[cfg(debug_assertions)]
642async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
643 let mut events = fs
644 .watch("styles/src".as_ref(), Duration::from_millis(100))
645 .await;
646 while (events.next().await).is_some() {
647 let output = Command::new("npm")
648 .current_dir("styles")
649 .args(["run", "build"])
650 .output()
651 .await
652 .log_err()?;
653 if output.status.success() {
654 cx.update(|cx| theme_selector::reload(cx))
655 } else {
656 eprintln!(
657 "build script failed {}",
658 String::from_utf8_lossy(&output.stderr)
659 );
660 }
661 }
662 Some(())
663}
664
665#[cfg(debug_assertions)]
666async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
667 let mut events = fs
668 .watch(
669 "crates/zed/src/languages".as_ref(),
670 Duration::from_millis(100),
671 )
672 .await;
673 while (events.next().await).is_some() {
674 languages.reload();
675 }
676 Some(())
677}
678
679#[cfg(not(debug_assertions))]
680async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
681 None
682}
683
684#[cfg(not(debug_assertions))]
685async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
686 None
687}
688
689fn connect_to_cli(
690 server_name: &str,
691) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
692 let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
693 .context("error connecting to cli")?;
694 let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
695 let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
696
697 handshake_tx
698 .send(IpcHandshake {
699 requests: request_tx,
700 responses: response_rx,
701 })
702 .context("error sending ipc handshake")?;
703
704 let (mut async_request_tx, async_request_rx) =
705 futures::channel::mpsc::channel::<CliRequest>(16);
706 thread::spawn(move || {
707 while let Ok(cli_request) = request_rx.recv() {
708 if smol::block_on(async_request_tx.send(cli_request)).is_err() {
709 break;
710 }
711 }
712 Ok::<_, anyhow::Error>(())
713 });
714
715 Ok((async_request_rx, response_tx))
716}
717
718async fn handle_cli_connection(
719 (mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
720 app_state: Arc<AppState>,
721 mut cx: AsyncAppContext,
722) {
723 if let Some(request) = requests.next().await {
724 match request {
725 CliRequest::Open { paths, wait } => {
726 let mut caret_positions = HashMap::new();
727
728 let paths = if paths.is_empty() {
729 workspace::last_opened_workspace_paths()
730 .await
731 .map(|location| location.paths().to_vec())
732 .unwrap_or_default()
733 } else {
734 paths
735 .into_iter()
736 .filter_map(|path_with_position_string| {
737 let path_with_position = PathLikeWithPosition::parse_str(
738 &path_with_position_string,
739 |path_str| {
740 Ok::<_, std::convert::Infallible>(
741 Path::new(path_str).to_path_buf(),
742 )
743 },
744 )
745 .expect("Infallible");
746 let path = path_with_position.path_like;
747 if let Some(row) = path_with_position.row {
748 if path.is_file() {
749 let row = row.saturating_sub(1);
750 let col =
751 path_with_position.column.unwrap_or(0).saturating_sub(1);
752 caret_positions.insert(path.clone(), Point::new(row, col));
753 }
754 }
755 Some(path)
756 })
757 .collect()
758 };
759
760 let mut errored = false;
761 match cx
762 .update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
763 .await
764 {
765 Ok((workspace, items)) => {
766 let mut item_release_futures = Vec::new();
767
768 for (item, path) in items.into_iter().zip(&paths) {
769 match item {
770 Some(Ok(item)) => {
771 if let Some(point) = caret_positions.remove(path) {
772 if let Some(active_editor) = item.downcast::<Editor>() {
773 active_editor
774 .downgrade()
775 .update(&mut cx, |editor, cx| {
776 let snapshot =
777 editor.snapshot(cx).display_snapshot;
778 let point = snapshot
779 .buffer_snapshot
780 .clip_point(point, Bias::Left);
781 editor.change_selections(
782 Some(Autoscroll::center()),
783 cx,
784 |s| s.select_ranges([point..point]),
785 );
786 })
787 .log_err();
788 }
789 }
790
791 let released = oneshot::channel();
792 cx.update(|cx| {
793 item.on_release(
794 cx,
795 Box::new(move |_| {
796 let _ = released.0.send(());
797 }),
798 )
799 .detach();
800 });
801 item_release_futures.push(released.1);
802 }
803 Some(Err(err)) => {
804 responses
805 .send(CliResponse::Stderr {
806 message: format!("error opening {:?}: {}", path, err),
807 })
808 .log_err();
809 errored = true;
810 }
811 None => {}
812 }
813 }
814
815 if wait {
816 let background = cx.background();
817 let wait = async move {
818 if paths.is_empty() {
819 let (done_tx, done_rx) = oneshot::channel();
820 if let Some(workspace) = workspace.upgrade(&cx) {
821 let _subscription = cx.update(|cx| {
822 cx.observe_release(&workspace, move |_, _| {
823 let _ = done_tx.send(());
824 })
825 });
826 drop(workspace);
827 let _ = done_rx.await;
828 }
829 } else {
830 let _ =
831 futures::future::try_join_all(item_release_futures).await;
832 };
833 }
834 .fuse();
835 futures::pin_mut!(wait);
836
837 loop {
838 // Repeatedly check if CLI is still open to avoid wasting resources
839 // waiting for files or workspaces to close.
840 let mut timer = background.timer(Duration::from_secs(1)).fuse();
841 futures::select_biased! {
842 _ = wait => break,
843 _ = timer => {
844 if responses.send(CliResponse::Ping).is_err() {
845 break;
846 }
847 }
848 }
849 }
850 }
851 }
852 Err(error) => {
853 errored = true;
854 responses
855 .send(CliResponse::Stderr {
856 message: format!("error opening {:?}: {}", paths, error),
857 })
858 .log_err();
859 }
860 }
861
862 responses
863 .send(CliResponse::Exit {
864 status: i32::from(errored),
865 })
866 .log_err();
867 }
868 }
869 }
870}
871
872pub fn dock_default_item_factory(
873 workspace: &mut Workspace,
874 cx: &mut ViewContext<Workspace>,
875) -> Option<Box<dyn ItemHandle>> {
876 let strategy = settings::get::<TerminalSettings>(cx)
877 .working_directory
878 .clone();
879 let working_directory = get_working_directory(workspace, cx, strategy);
880
881 let window_id = cx.window_id();
882 let terminal = workspace
883 .project()
884 .update(cx, |project, cx| {
885 project.create_terminal(working_directory, window_id, cx)
886 })
887 .notify_err(workspace, cx)?;
888
889 let terminal_view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
890
891 Some(Box::new(terminal_view))
892}
893
894pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
895 &[
896 ("Go to file", &file_finder::Toggle),
897 ("Open command palette", &command_palette::Toggle),
898 ("Open recent projects", &recent_projects::OpenRecent),
899 ("Change your settings", &zed::OpenSettings),
900 ]
901}