1mod app_menus;
2pub mod component_preview;
3pub mod edit_prediction_registry;
4#[cfg(target_os = "macos")]
5pub(crate) mod mac_only_instance;
6mod migrate;
7mod open_listener;
8mod quick_action_bar;
9#[cfg(target_os = "windows")]
10pub(crate) mod windows_only_instance;
11
12use agent_ui::{AgentDiffToolbar, AgentPanelDelegate};
13use anyhow::Context as _;
14pub use app_menus::*;
15use assets::Assets;
16use audio::{AudioSettings, REPLAY_DURATION};
17use breadcrumbs::Breadcrumbs;
18use client::zed_urls;
19use collections::VecDeque;
20use debugger_ui::debugger_panel::DebugPanel;
21use editor::{Editor, MultiBuffer};
22use extension_host::ExtensionStore;
23use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
24use fs::Fs;
25use futures::future::Either;
26use futures::{StreamExt, channel::mpsc, select_biased};
27use git_ui::commit_view::CommitViewToolbar;
28use git_ui::git_panel::GitPanel;
29use git_ui::project_diff::ProjectDiffToolbar;
30use gpui::{
31 Action, App, AppContext as _, Context, DismissEvent, Element, Entity, Focusable, KeyBinding,
32 ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, Styled, Task,
33 TitlebarOptions, UpdateGlobal, Window, WindowKind, WindowOptions, actions, image_cache, point,
34 px, retain_all,
35};
36use image_viewer::ImageInfo;
37use language::Capability;
38use language_onboarding::BasedPyrightBanner;
39use language_tools::lsp_button::{self, LspButton};
40use language_tools::lsp_log_view::LspLogToolbarItemView;
41use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
42use migrator::migrate_keymap;
43use onboarding::DOCS_URL;
44use onboarding::multibuffer_hint::MultibufferHint;
45pub use open_listener::*;
46use outline_panel::OutlinePanel;
47use paths::{
48 local_debug_file_relative_path, local_settings_file_relative_path,
49 local_tasks_file_relative_path,
50};
51use project::{DirectoryLister, DisableAiSettings, ProjectItem};
52use project_panel::ProjectPanel;
53use prompt_store::PromptBuilder;
54use quick_action_bar::QuickActionBar;
55use recent_projects::open_remote_project;
56use release_channel::{AppCommitSha, ReleaseChannel};
57use rope::Rope;
58use search::project_search::ProjectSearchBar;
59use settings::{
60 BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile,
61 KeymapFileLoadResult, Settings, SettingsStore, VIM_KEYMAP_PATH,
62 initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
63 update_settings_file,
64};
65use std::time::Duration;
66use std::{
67 borrow::Cow,
68 path::{Path, PathBuf},
69 sync::Arc,
70 sync::atomic::{self, AtomicBool},
71};
72use terminal_view::terminal_panel::{self, TerminalPanel};
73use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
74use ui::{PopoverMenuHandle, prelude::*};
75use util::markdown::MarkdownString;
76use util::rel_path::RelPath;
77use util::{ResultExt, asset_str};
78use uuid::Uuid;
79use vim_mode_setting::VimModeSetting;
80use workspace::notifications::{
81 NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification,
82};
83use workspace::{
84 AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
85 create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
86 open_new,
87};
88use workspace::{
89 CloseIntent, CloseWindow, NotificationFrame, RestoreBanner, with_active_or_new_workspace,
90};
91use workspace::{Pane, notifications::DetachAndPromptErr};
92use zed_actions::{
93 OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile, OpenZedUrl,
94 Quit,
95};
96
97actions!(
98 zed,
99 [
100 /// Opens the element inspector for debugging UI.
101 DebugElements,
102 /// Hides the application window.
103 Hide,
104 /// Hides all other application windows.
105 HideOthers,
106 /// Minimizes the current window.
107 Minimize,
108 /// Opens the default settings file.
109 OpenDefaultSettings,
110 /// Opens project-specific settings file.
111 OpenProjectSettingsFile,
112 /// Opens the project tasks configuration.
113 OpenProjectTasks,
114 /// Opens the tasks panel.
115 OpenTasks,
116 /// Opens debug tasks configuration.
117 OpenDebugTasks,
118 /// Resets the application database.
119 ResetDatabase,
120 /// Shows all hidden windows.
121 ShowAll,
122 /// Toggles fullscreen mode.
123 ToggleFullScreen,
124 /// Zooms the window.
125 Zoom,
126 /// Triggers a test panic for debugging.
127 TestPanic,
128 /// Triggers a hard crash for debugging.
129 TestCrash,
130 ]
131);
132
133actions!(
134 dev,
135 [
136 /// Stores last 30s of audio from zed staff using the experimental rodio
137 /// audio system (including yourself) on the current call in a tar file
138 /// in the current working directory.
139 CaptureRecentAudio,
140 ]
141);
142
143pub fn init(cx: &mut App) {
144 #[cfg(target_os = "macos")]
145 cx.on_action(|_: &Hide, cx| cx.hide());
146 #[cfg(target_os = "macos")]
147 cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps());
148 #[cfg(target_os = "macos")]
149 cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps());
150 cx.on_action(quit);
151
152 cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx));
153 let flag = cx.wait_for_flag::<PanicFeatureFlag>();
154 cx.spawn(async |cx| {
155 if cx
156 .update(|cx| ReleaseChannel::global(cx) == ReleaseChannel::Dev)
157 .unwrap_or_default()
158 || flag.await
159 {
160 cx.update(|cx| {
161 cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action"));
162 cx.on_action(|_: &TestCrash, _| {
163 unsafe extern "C" {
164 fn puts(s: *const i8);
165 }
166 unsafe {
167 puts(0xabad1d3a as *const i8);
168 }
169 });
170 })
171 .ok();
172 };
173 })
174 .detach();
175 cx.on_action(|_: &OpenLog, cx| {
176 with_active_or_new_workspace(cx, |workspace, window, cx| {
177 open_log_file(workspace, window, cx);
178 });
179 });
180 cx.on_action(|_: &workspace::RevealLogInFileManager, cx| {
181 cx.reveal_path(paths::log_file().as_path());
182 });
183 cx.on_action(|_: &zed_actions::OpenLicenses, cx| {
184 with_active_or_new_workspace(cx, |workspace, window, cx| {
185 open_bundled_file(
186 workspace,
187 asset_str::<Assets>("licenses.md"),
188 "Open Source License Attribution",
189 "Markdown",
190 window,
191 cx,
192 );
193 });
194 });
195 cx.on_action(|_: &zed_actions::OpenTelemetryLog, cx| {
196 with_active_or_new_workspace(cx, |workspace, window, cx| {
197 open_telemetry_log_file(workspace, window, cx);
198 });
199 });
200 cx.on_action(|&zed_actions::OpenKeymapFile, cx| {
201 with_active_or_new_workspace(cx, |_, window, cx| {
202 open_settings_file(
203 paths::keymap_file(),
204 || settings::initial_keymap_content().as_ref().into(),
205 window,
206 cx,
207 );
208 });
209 });
210 cx.on_action(|_: &OpenSettingsFile, cx| {
211 with_active_or_new_workspace(cx, |_, window, cx| {
212 open_settings_file(
213 paths::settings_file(),
214 || settings::initial_user_settings_content().as_ref().into(),
215 window,
216 cx,
217 );
218 });
219 });
220 cx.on_action(|_: &OpenAccountSettings, cx| {
221 with_active_or_new_workspace(cx, |_, _, cx| {
222 cx.open_url(&zed_urls::account_url(cx));
223 });
224 });
225 cx.on_action(|_: &OpenTasks, cx| {
226 with_active_or_new_workspace(cx, |_, window, cx| {
227 open_settings_file(
228 paths::tasks_file(),
229 || settings::initial_tasks_content().as_ref().into(),
230 window,
231 cx,
232 );
233 });
234 });
235 cx.on_action(|_: &OpenDebugTasks, cx| {
236 with_active_or_new_workspace(cx, |_, window, cx| {
237 open_settings_file(
238 paths::debug_scenarios_file(),
239 || settings::initial_debug_tasks_content().as_ref().into(),
240 window,
241 cx,
242 );
243 });
244 });
245 cx.on_action(|_: &OpenDefaultSettings, cx| {
246 with_active_or_new_workspace(cx, |workspace, window, cx| {
247 open_bundled_file(
248 workspace,
249 settings::default_settings(),
250 "Default Settings",
251 "JSON",
252 window,
253 cx,
254 );
255 });
256 });
257 cx.on_action(|_: &zed_actions::OpenDefaultKeymap, cx| {
258 with_active_or_new_workspace(cx, |workspace, window, cx| {
259 open_bundled_file(
260 workspace,
261 settings::default_keymap(),
262 "Default Key Bindings",
263 "JSON",
264 window,
265 cx,
266 );
267 });
268 });
269 cx.on_action(|_: &zed_actions::About, cx| {
270 with_active_or_new_workspace(cx, |workspace, window, cx| {
271 about(workspace, window, cx);
272 });
273 });
274}
275
276fn bind_on_window_closed(cx: &mut App) -> Option<gpui::Subscription> {
277 #[cfg(target_os = "macos")]
278 {
279 WorkspaceSettings::get_global(cx)
280 .on_last_window_closed
281 .is_quit_app()
282 .then(|| {
283 cx.on_window_closed(|cx| {
284 if cx.windows().is_empty() {
285 cx.quit();
286 }
287 })
288 })
289 }
290 #[cfg(not(target_os = "macos"))]
291 {
292 Some(cx.on_window_closed(|cx| {
293 if cx.windows().is_empty() {
294 cx.quit();
295 }
296 }))
297 }
298}
299
300pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut App) -> WindowOptions {
301 let display = display_uuid.and_then(|uuid| {
302 cx.displays()
303 .into_iter()
304 .find(|display| display.uuid().ok() == Some(uuid))
305 });
306 let app_id = ReleaseChannel::global(cx).app_id();
307 let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
308 Ok(val) if val == "server" => gpui::WindowDecorations::Server,
309 Ok(val) if val == "client" => gpui::WindowDecorations::Client,
310 _ => gpui::WindowDecorations::Client,
311 };
312
313 let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs;
314
315 WindowOptions {
316 titlebar: Some(TitlebarOptions {
317 title: None,
318 appears_transparent: true,
319 traffic_light_position: Some(point(px(9.0), px(9.0))),
320 }),
321 window_bounds: None,
322 focus: false,
323 show: false,
324 kind: WindowKind::Normal,
325 is_movable: true,
326 display_id: display.map(|display| display.id()),
327 window_background: cx.theme().window_background_appearance(),
328 app_id: Some(app_id.to_owned()),
329 window_decorations: Some(window_decorations),
330 window_min_size: Some(gpui::Size {
331 width: px(360.0),
332 height: px(240.0),
333 }),
334 tabbing_identifier: if use_system_window_tabs {
335 Some(String::from("zed"))
336 } else {
337 None
338 },
339 ..Default::default()
340 }
341}
342
343pub fn initialize_workspace(
344 app_state: Arc<AppState>,
345 prompt_builder: Arc<PromptBuilder>,
346 cx: &mut App,
347) {
348 let mut _on_close_subscription = bind_on_window_closed(cx);
349 cx.observe_global::<SettingsStore>(move |cx| {
350 _on_close_subscription = bind_on_window_closed(cx);
351 })
352 .detach();
353
354 cx.observe_new(move |workspace: &mut Workspace, window, cx| {
355 let Some(window) = window else {
356 return;
357 };
358
359 let workspace_handle = cx.entity();
360 let center_pane = workspace.active_pane().clone();
361 initialize_pane(workspace, ¢er_pane, window, cx);
362
363 cx.subscribe_in(&workspace_handle, window, {
364 move |workspace, _, event, window, cx| match event {
365 workspace::Event::PaneAdded(pane) => {
366 initialize_pane(workspace, pane, window, cx);
367 }
368 workspace::Event::OpenBundledFile {
369 text,
370 title,
371 language,
372 } => open_bundled_file(workspace, text.clone(), title, language, window, cx),
373 _ => {}
374 }
375 })
376 .detach();
377
378 #[cfg(not(target_os = "macos"))]
379 initialize_file_watcher(window, cx);
380
381 if let Some(specs) = window.gpu_specs() {
382 log::info!("Using GPU: {:?}", specs);
383 show_software_emulation_warning_if_needed(specs.clone(), window, cx);
384 if let Some((crash_server, message)) = crashes::CRASH_HANDLER
385 .get()
386 .zip(bincode::serialize(&specs).ok())
387 && let Err(err) = crash_server.send_message(3, message)
388 {
389 log::warn!(
390 "Failed to store active gpu info for crash reporting: {}",
391 err
392 );
393 }
394 }
395
396 #[cfg(target_os = "windows")]
397 unstable_version_notification(cx);
398
399 let edit_prediction_menu_handle = PopoverMenuHandle::default();
400 let edit_prediction_button = cx.new(|cx| {
401 edit_prediction_button::EditPredictionButton::new(
402 app_state.fs.clone(),
403 app_state.user_store.clone(),
404 edit_prediction_menu_handle.clone(),
405 app_state.client.clone(),
406 cx,
407 )
408 });
409 workspace.register_action({
410 move |_, _: &edit_prediction_button::ToggleMenu, window, cx| {
411 edit_prediction_menu_handle.toggle(window, cx);
412 }
413 });
414
415 let search_button = cx.new(|_| search::search_status_button::SearchButton::new());
416 let diagnostic_summary =
417 cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
418 let activity_indicator = activity_indicator::ActivityIndicator::new(
419 workspace,
420 workspace.project().read(cx).languages().clone(),
421 window,
422 cx,
423 );
424 let active_buffer_language =
425 cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace));
426 let active_toolchain_language =
427 cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
428 let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
429 let image_info = cx.new(|_cx| ImageInfo::new(workspace));
430
431 let lsp_button_menu_handle = PopoverMenuHandle::default();
432 let lsp_button =
433 cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx));
434 workspace.register_action({
435 move |_, _: &lsp_button::ToggleMenu, window, cx| {
436 lsp_button_menu_handle.toggle(window, cx);
437 }
438 });
439
440 let cursor_position =
441 cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
442 let line_ending_indicator =
443 cx.new(|_| line_ending_selector::LineEndingIndicator::default());
444 workspace.status_bar().update(cx, |status_bar, cx| {
445 status_bar.add_left_item(search_button, window, cx);
446 status_bar.add_left_item(lsp_button, window, cx);
447 status_bar.add_left_item(diagnostic_summary, window, cx);
448 status_bar.add_left_item(activity_indicator, window, cx);
449 status_bar.add_right_item(edit_prediction_button, window, cx);
450 status_bar.add_right_item(active_buffer_language, window, cx);
451 status_bar.add_right_item(active_toolchain_language, window, cx);
452 status_bar.add_right_item(line_ending_indicator, window, cx);
453 status_bar.add_right_item(vim_mode_indicator, window, cx);
454 status_bar.add_right_item(cursor_position, window, cx);
455 status_bar.add_right_item(image_info, window, cx);
456 });
457
458 let handle = cx.entity().downgrade();
459 window.on_window_should_close(cx, move |window, cx| {
460 handle
461 .update(cx, |workspace, cx| {
462 // We'll handle closing asynchronously
463 workspace.close_window(&CloseWindow, window, cx);
464 false
465 })
466 .unwrap_or(true)
467 });
468
469 initialize_panels(prompt_builder.clone(), window, cx);
470 register_actions(app_state.clone(), workspace, window, cx);
471
472 workspace.focus_handle(cx).focus(window);
473 })
474 .detach();
475}
476
477#[cfg(target_os = "windows")]
478fn unstable_version_notification(cx: &mut App) {
479 if !matches!(
480 ReleaseChannel::try_global(cx),
481 Some(ReleaseChannel::Nightly)
482 ) {
483 return;
484 }
485 let db_key = "zed_windows_nightly_notif_shown_at".to_owned();
486 let time = chrono::Utc::now();
487 if let Some(last_shown) = db::kvp::KEY_VALUE_STORE
488 .read_kvp(&db_key)
489 .log_err()
490 .flatten()
491 .and_then(|timestamp| chrono::DateTime::parse_from_rfc3339(×tamp).ok())
492 {
493 if time.fixed_offset() - last_shown < chrono::Duration::days(7) {
494 return;
495 }
496 }
497 cx.spawn(async move |_| {
498 db::kvp::KEY_VALUE_STORE
499 .write_kvp(db_key, time.to_rfc3339())
500 .await
501 })
502 .detach_and_log_err(cx);
503 struct WindowsNightly;
504 show_app_notification(NotificationId::unique::<WindowsNightly>(), cx, |cx| {
505 cx.new(|cx| {
506 MessageNotification::new("You're using an unstable version of Zed (Nightly)", cx)
507 .primary_message("Download Stable")
508 .primary_icon_color(Color::Accent)
509 .primary_icon(IconName::Download)
510 .primary_on_click(|window, cx| {
511 window.dispatch_action(
512 zed_actions::OpenBrowser {
513 url: "https://zed.dev/download".to_string(),
514 }
515 .boxed_clone(),
516 cx,
517 );
518 cx.emit(DismissEvent);
519 })
520 })
521 });
522}
523
524#[cfg(any(target_os = "linux", target_os = "freebsd"))]
525fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
526 if let Err(e) = fs::fs_watcher::global(|_| {}) {
527 let message = format!(
528 db::indoc! {r#"
529 inotify_init returned {}
530
531 This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux
532 "#},
533 e
534 );
535 let prompt = window.prompt(
536 PromptLevel::Critical,
537 "Could not start inotify",
538 Some(&message),
539 &["Troubleshoot and Quit"],
540 cx,
541 );
542 cx.spawn(async move |_, cx| {
543 if prompt.await == Ok(0) {
544 cx.update(|cx| {
545 cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify");
546 cx.quit();
547 })
548 .ok();
549 }
550 })
551 .detach()
552 }
553}
554
555#[cfg(target_os = "windows")]
556fn initialize_file_watcher(window: &mut Window, cx: &mut Context<Workspace>) {
557 if let Err(e) = fs::fs_watcher::global(|_| {}) {
558 let message = format!(
559 db::indoc! {r#"
560 ReadDirectoryChangesW initialization failed: {}
561
562 This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows
563 "#},
564 e
565 );
566 let prompt = window.prompt(
567 PromptLevel::Critical,
568 "Could not start ReadDirectoryChangesW",
569 Some(&message),
570 &["Troubleshoot and Quit"],
571 cx,
572 );
573 cx.spawn(async move |_, cx| {
574 if prompt.await == Ok(0) {
575 cx.update(|cx| {
576 cx.open_url("https://zed.dev/docs/windows");
577 cx.quit()
578 })
579 .ok();
580 }
581 })
582 .detach()
583 }
584}
585
586fn show_software_emulation_warning_if_needed(
587 specs: gpui::GpuSpecs,
588 window: &mut Window,
589 cx: &mut Context<Workspace>,
590) {
591 if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() {
592 let (graphics_api, docs_url, open_url) = if cfg!(target_os = "windows") {
593 (
594 "DirectX",
595 "https://zed.dev/docs/windows",
596 "https://zed.dev/docs/windows",
597 )
598 } else {
599 (
600 "Vulkan",
601 "https://zed.dev/docs/linux",
602 "https://zed.dev/docs/linux#zed-fails-to-open-windows",
603 )
604 };
605 let message = format!(
606 db::indoc! {r#"
607 Zed uses {} for rendering and requires a compatible GPU.
608
609 Currently you are using a software emulated GPU ({}) which
610 will result in awful performance.
611
612 For troubleshooting see: {}
613 Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override.
614 "#},
615 graphics_api, specs.device_name, docs_url
616 );
617 let prompt = window.prompt(
618 PromptLevel::Critical,
619 "Unsupported GPU",
620 Some(&message),
621 &["Skip", "Troubleshoot and Quit"],
622 cx,
623 );
624 cx.spawn(async move |_, cx| {
625 if prompt.await == Ok(1) {
626 cx.update(|cx| {
627 cx.open_url(open_url);
628 cx.quit();
629 })
630 .ok();
631 }
632 })
633 .detach()
634 }
635}
636
637fn initialize_panels(
638 prompt_builder: Arc<PromptBuilder>,
639 window: &mut Window,
640 cx: &mut Context<Workspace>,
641) {
642 cx.spawn_in(window, async move |workspace_handle, cx| {
643 let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
644 let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
645 let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
646 let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone());
647 let channels_panel =
648 collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
649 let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
650 workspace_handle.clone(),
651 cx.clone(),
652 );
653 let debug_panel = DebugPanel::load(workspace_handle.clone(), cx);
654
655 let (
656 project_panel,
657 outline_panel,
658 terminal_panel,
659 git_panel,
660 channels_panel,
661 notification_panel,
662 debug_panel,
663 ) = futures::try_join!(
664 project_panel,
665 outline_panel,
666 git_panel,
667 terminal_panel,
668 channels_panel,
669 notification_panel,
670 debug_panel,
671 )?;
672
673 workspace_handle.update_in(cx, |workspace, window, cx| {
674 workspace.add_panel(project_panel, window, cx);
675 workspace.add_panel(outline_panel, window, cx);
676 workspace.add_panel(terminal_panel, window, cx);
677 workspace.add_panel(git_panel, window, cx);
678 workspace.add_panel(channels_panel, window, cx);
679 workspace.add_panel(notification_panel, window, cx);
680 workspace.add_panel(debug_panel, window, cx);
681 })?;
682
683 fn setup_or_teardown_agent_panel(
684 workspace: &mut Workspace,
685 prompt_builder: Arc<PromptBuilder>,
686 window: &mut Window,
687 cx: &mut Context<Workspace>,
688 ) -> Task<anyhow::Result<()>> {
689 let disable_ai = SettingsStore::global(cx)
690 .get::<DisableAiSettings>(None)
691 .disable_ai
692 || cfg!(test);
693 let existing_panel = workspace.panel::<agent_ui::AgentPanel>(cx);
694 match (disable_ai, existing_panel) {
695 (false, None) => cx.spawn_in(window, async move |workspace, cx| {
696 let panel =
697 agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone())
698 .await?;
699 workspace.update_in(cx, |workspace, window, cx| {
700 let disable_ai = SettingsStore::global(cx)
701 .get::<DisableAiSettings>(None)
702 .disable_ai;
703 let have_panel = workspace.panel::<agent_ui::AgentPanel>(cx).is_some();
704 if !disable_ai && !have_panel {
705 workspace.add_panel(panel, window, cx);
706 }
707 })
708 }),
709 (true, Some(existing_panel)) => {
710 workspace.remove_panel::<agent_ui::AgentPanel>(&existing_panel, window, cx);
711 Task::ready(Ok(()))
712 }
713 _ => Task::ready(Ok(())),
714 }
715 }
716
717 workspace_handle
718 .update_in(cx, |workspace, window, cx| {
719 setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx)
720 })?
721 .await?;
722
723 workspace_handle.update_in(cx, |workspace, window, cx| {
724 cx.observe_global_in::<SettingsStore>(window, {
725 let prompt_builder = prompt_builder.clone();
726 move |workspace, window, cx| {
727 setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx)
728 .detach_and_log_err(cx);
729 }
730 })
731 .detach();
732
733 // Register the actions that are shared between `assistant` and `assistant2`.
734 //
735 // We need to do this here instead of within the individual `init`
736 // functions so that we only register the actions once.
737 //
738 // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
739 if !cfg!(test) {
740 <dyn AgentPanelDelegate>::set_global(
741 Arc::new(agent_ui::ConcreteAssistantPanelDelegate),
742 cx,
743 );
744
745 workspace
746 .register_action(agent_ui::AgentPanel::toggle_focus)
747 .register_action(agent_ui::InlineAssistant::inline_assist);
748 }
749 })?;
750
751 anyhow::Ok(())
752 })
753 .detach();
754}
755
756fn register_actions(
757 app_state: Arc<AppState>,
758 workspace: &mut Workspace,
759 _: &mut Window,
760 cx: &mut Context<Workspace>,
761) {
762 workspace
763 .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL))
764 .register_action(|_, _: &Minimize, window, _| {
765 window.minimize_window();
766 })
767 .register_action(|_, _: &Zoom, window, _| {
768 window.zoom_window();
769 })
770 .register_action(|_, _: &ToggleFullScreen, window, _| {
771 window.toggle_fullscreen();
772 })
773 .register_action(|_, action: &OpenZedUrl, _, cx| {
774 OpenListener::global(cx).open(RawOpenRequest {
775 urls: vec![action.url.clone()],
776 ..Default::default()
777 })
778 })
779 .register_action(|workspace, action: &OpenBrowser, _window, cx| {
780 // Parse and validate the URL to ensure it's properly formatted
781 match url::Url::parse(&action.url) {
782 Ok(parsed_url) => {
783 // Use the parsed URL's string representation which is properly escaped
784 cx.open_url(parsed_url.as_str());
785 }
786 Err(e) => {
787 workspace.show_error(
788 &anyhow::anyhow!(
789 "Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}",
790 action.url
791 ),
792 cx,
793 );
794 }
795 }
796 })
797 .register_action(|workspace, _: &workspace::Open, window, cx| {
798 telemetry::event!("Project Opened");
799 let paths = workspace.prompt_for_open_path(
800 PathPromptOptions {
801 files: true,
802 directories: true,
803 multiple: true,
804 prompt: None,
805 },
806 DirectoryLister::Local(
807 workspace.project().clone(),
808 workspace.app_state().fs.clone(),
809 ),
810 window,
811 cx,
812 );
813
814 cx.spawn_in(window, async move |this, cx| {
815 let Some(paths) = paths.await.log_err().flatten() else {
816 return;
817 };
818
819 if let Some(task) = this
820 .update_in(cx, |this, window, cx| {
821 this.open_workspace_for_paths(false, paths, window, cx)
822 })
823 .log_err()
824 {
825 task.await.log_err();
826 }
827 })
828 .detach()
829 })
830 .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| {
831 if !action.from_existing_connection {
832 cx.propagate();
833 return;
834 }
835 // You need existing remote connection to open it this way
836 if workspace.project().read(cx).is_local() {
837 return;
838 }
839 telemetry::event!("Project Opened");
840 let paths = workspace.prompt_for_open_path(
841 PathPromptOptions {
842 files: true,
843 directories: true,
844 multiple: true,
845 prompt: None,
846 },
847 DirectoryLister::Project(workspace.project().clone()),
848 window,
849 cx,
850 );
851 cx.spawn_in(window, async move |this, cx| {
852 let Some(paths) = paths.await.log_err().flatten() else {
853 return;
854 };
855 if let Some(task) = this
856 .update_in(cx, |this, window, cx| {
857 open_new_ssh_project_from_project(this, paths, window, cx)
858 })
859 .log_err()
860 {
861 task.await.log_err();
862 }
863 })
864 .detach()
865 })
866 .register_action({
867 let fs = app_state.fs.clone();
868 move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| {
869 if action.persist {
870 update_settings_file(fs.clone(), cx, move |settings, cx| {
871 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0);
872 let _ = settings
873 .theme
874 .ui_font_size
875 .insert(theme::clamp_font_size(ui_font_size).into());
876 });
877 } else {
878 theme::adjust_ui_font_size(cx, |size| size + px(1.0));
879 }
880 }
881 })
882 .register_action({
883 let fs = app_state.fs.clone();
884 move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| {
885 if action.persist {
886 update_settings_file(fs.clone(), cx, move |settings, cx| {
887 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0);
888 let _ = settings
889 .theme
890 .ui_font_size
891 .insert(theme::clamp_font_size(ui_font_size).into());
892 });
893 } else {
894 theme::adjust_ui_font_size(cx, |size| size - px(1.0));
895 }
896 }
897 })
898 .register_action({
899 let fs = app_state.fs.clone();
900 move |_, action: &zed_actions::ResetUiFontSize, _window, cx| {
901 if action.persist {
902 update_settings_file(fs.clone(), cx, move |settings, _| {
903 settings.theme.ui_font_size = None;
904 });
905 } else {
906 theme::reset_ui_font_size(cx);
907 }
908 }
909 })
910 .register_action({
911 let fs = app_state.fs.clone();
912 move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| {
913 if action.persist {
914 update_settings_file(fs.clone(), cx, move |settings, cx| {
915 let buffer_font_size =
916 ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0);
917 let _ = settings
918 .theme
919 .buffer_font_size
920 .insert(theme::clamp_font_size(buffer_font_size).into());
921 });
922 } else {
923 theme::adjust_buffer_font_size(cx, |size| size + px(1.0));
924 }
925 }
926 })
927 .register_action({
928 let fs = app_state.fs.clone();
929 move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| {
930 if action.persist {
931 update_settings_file(fs.clone(), cx, move |settings, cx| {
932 let buffer_font_size =
933 ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0);
934 let _ = settings
935 .theme
936 .buffer_font_size
937 .insert(theme::clamp_font_size(buffer_font_size).into());
938 });
939 } else {
940 theme::adjust_buffer_font_size(cx, |size| size - px(1.0));
941 }
942 }
943 })
944 .register_action({
945 let fs = app_state.fs.clone();
946 move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| {
947 if action.persist {
948 update_settings_file(fs.clone(), cx, move |settings, _| {
949 settings.theme.buffer_font_size = None;
950 });
951 } else {
952 theme::reset_buffer_font_size(cx);
953 }
954 }
955 })
956 .register_action({
957 let fs = app_state.fs.clone();
958 move |_, action: &zed_actions::ResetAllZoom, _window, cx| {
959 if action.persist {
960 update_settings_file(fs.clone(), cx, move |settings, _| {
961 settings.theme.ui_font_size = None;
962 settings.theme.buffer_font_size = None;
963 settings.theme.agent_ui_font_size = None;
964 settings.theme.agent_buffer_font_size = None;
965 });
966 } else {
967 theme::reset_ui_font_size(cx);
968 theme::reset_buffer_font_size(cx);
969 theme::reset_agent_ui_font_size(cx);
970 theme::reset_agent_buffer_font_size(cx);
971 }
972 }
973 })
974 .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| {
975 cx.spawn_in(window, async move |workspace, cx| {
976 install_cli::register_zed_scheme(cx).await?;
977 workspace.update_in(cx, |workspace, _, cx| {
978 struct RegisterZedScheme;
979
980 workspace.show_toast(
981 Toast::new(
982 NotificationId::unique::<RegisterZedScheme>(),
983 format!(
984 "zed:// links will now open in {}.",
985 ReleaseChannel::global(cx).display_name()
986 ),
987 ),
988 cx,
989 )
990 })?;
991 Ok(())
992 })
993 .detach_and_prompt_err(
994 "Error registering zed:// scheme",
995 window,
996 cx,
997 |_, _, _| None,
998 );
999 })
1000 .register_action(open_project_settings_file)
1001 .register_action(open_project_tasks_file)
1002 .register_action(open_project_debug_tasks_file)
1003 .register_action(
1004 |workspace: &mut Workspace,
1005 _: &project_panel::ToggleFocus,
1006 window: &mut Window,
1007 cx: &mut Context<Workspace>| {
1008 workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
1009 },
1010 )
1011 .register_action(
1012 |workspace: &mut Workspace,
1013 _: &outline_panel::ToggleFocus,
1014 window: &mut Window,
1015 cx: &mut Context<Workspace>| {
1016 workspace.toggle_panel_focus::<OutlinePanel>(window, cx);
1017 },
1018 )
1019 .register_action(
1020 |workspace: &mut Workspace,
1021 _: &collab_ui::collab_panel::ToggleFocus,
1022 window: &mut Window,
1023 cx: &mut Context<Workspace>| {
1024 workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(window, cx);
1025 },
1026 )
1027 .register_action(
1028 |workspace: &mut Workspace,
1029 _: &collab_ui::notification_panel::ToggleFocus,
1030 window: &mut Window,
1031 cx: &mut Context<Workspace>| {
1032 workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(
1033 window, cx,
1034 );
1035 },
1036 )
1037 .register_action(
1038 |workspace: &mut Workspace,
1039 _: &terminal_panel::ToggleFocus,
1040 window: &mut Window,
1041 cx: &mut Context<Workspace>| {
1042 workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
1043 },
1044 )
1045 .register_action({
1046 let app_state = Arc::downgrade(&app_state);
1047 move |_, _: &NewWindow, _, cx| {
1048 if let Some(app_state) = app_state.upgrade() {
1049 open_new(
1050 Default::default(),
1051 app_state,
1052 cx,
1053 |workspace, window, cx| {
1054 cx.activate(true);
1055 Editor::new_file(workspace, &Default::default(), window, cx)
1056 },
1057 )
1058 .detach();
1059 }
1060 }
1061 })
1062 .register_action({
1063 let app_state = Arc::downgrade(&app_state);
1064 move |_, _: &NewFile, _, cx| {
1065 if let Some(app_state) = app_state.upgrade() {
1066 open_new(
1067 Default::default(),
1068 app_state,
1069 cx,
1070 |workspace, window, cx| {
1071 Editor::new_file(workspace, &Default::default(), window, cx)
1072 },
1073 )
1074 .detach();
1075 }
1076 }
1077 })
1078 .register_action(|workspace, _: &CaptureRecentAudio, window, cx| {
1079 capture_recent_audio(workspace, window, cx);
1080 });
1081
1082 #[cfg(not(target_os = "windows"))]
1083 workspace.register_action(install_cli);
1084
1085 if workspace.project().read(cx).is_via_remote_server() {
1086 workspace.register_action({
1087 move |workspace, _: &OpenServerSettings, window, cx| {
1088 let open_server_settings = workspace
1089 .project()
1090 .update(cx, |project, cx| project.open_server_settings(cx));
1091
1092 cx.spawn_in(window, async move |workspace, cx| {
1093 let buffer = open_server_settings.await?;
1094
1095 workspace
1096 .update_in(cx, |workspace, window, cx| {
1097 workspace.open_path(
1098 buffer
1099 .read(cx)
1100 .project_path(cx)
1101 .expect("Settings file must have a location"),
1102 None,
1103 true,
1104 window,
1105 cx,
1106 )
1107 })?
1108 .await?;
1109
1110 anyhow::Ok(())
1111 })
1112 .detach_and_log_err(cx);
1113 }
1114 });
1115 }
1116}
1117
1118fn initialize_pane(
1119 workspace: &Workspace,
1120 pane: &Entity<Pane>,
1121 window: &mut Window,
1122 cx: &mut Context<Workspace>,
1123) {
1124 pane.update(cx, |pane, cx| {
1125 pane.toolbar().update(cx, |toolbar, cx| {
1126 let multibuffer_hint = cx.new(|_| MultibufferHint::new());
1127 toolbar.add_item(multibuffer_hint, window, cx);
1128 let breadcrumbs = cx.new(|_| Breadcrumbs::new());
1129 toolbar.add_item(breadcrumbs, window, cx);
1130 let buffer_search_bar = cx.new(|cx| {
1131 search::BufferSearchBar::new(
1132 Some(workspace.project().read(cx).languages().clone()),
1133 window,
1134 cx,
1135 )
1136 });
1137 toolbar.add_item(buffer_search_bar.clone(), window, cx);
1138 let quick_action_bar =
1139 cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx));
1140 toolbar.add_item(quick_action_bar, window, cx);
1141 let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new());
1142 toolbar.add_item(diagnostic_editor_controls, window, cx);
1143 let project_search_bar = cx.new(|_| ProjectSearchBar::new());
1144 toolbar.add_item(project_search_bar, window, cx);
1145 let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new());
1146 toolbar.add_item(lsp_log_item, window, cx);
1147 let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new());
1148 toolbar.add_item(dap_log_item, window, cx);
1149 let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new());
1150 toolbar.add_item(acp_tools_item, window, cx);
1151 let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new());
1152 toolbar.add_item(syntax_tree_item, window, cx);
1153 let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));
1154 toolbar.add_item(migration_banner, window, cx);
1155 let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
1156 toolbar.add_item(project_diff_toolbar, window, cx);
1157 let commit_view_toolbar = cx.new(|cx| CommitViewToolbar::new(workspace, cx));
1158 toolbar.add_item(commit_view_toolbar, window, cx);
1159 let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
1160 toolbar.add_item(agent_diff_toolbar, window, cx);
1161 let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx));
1162 toolbar.add_item(basedpyright_banner, window, cx);
1163 })
1164 });
1165}
1166
1167fn about(_: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1168 let release_channel = ReleaseChannel::global(cx).display_name();
1169 let version = env!("CARGO_PKG_VERSION");
1170 let debug = if cfg!(debug_assertions) {
1171 "(debug)"
1172 } else {
1173 ""
1174 };
1175 let message = format!("{release_channel} {version} {debug}");
1176 let detail = AppCommitSha::try_global(cx).map(|sha| sha.full());
1177
1178 let prompt = window.prompt(
1179 PromptLevel::Info,
1180 &message,
1181 detail.as_deref(),
1182 &["Copy", "OK"],
1183 cx,
1184 );
1185 cx.spawn(async move |_, cx| {
1186 if let Ok(0) = prompt.await {
1187 let content = format!("{}\n{}", message, detail.as_deref().unwrap_or(""));
1188 cx.update(|cx| {
1189 cx.write_to_clipboard(gpui::ClipboardItem::new_string(content));
1190 })
1191 .ok();
1192 }
1193 })
1194 .detach();
1195}
1196
1197#[cfg(not(target_os = "windows"))]
1198fn install_cli(
1199 _: &mut Workspace,
1200 _: &install_cli::InstallCliBinary,
1201 window: &mut Window,
1202 cx: &mut Context<Workspace>,
1203) {
1204 install_cli::install_cli_binary(window, cx)
1205}
1206
1207static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false);
1208fn quit(_: &Quit, cx: &mut App) {
1209 if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) {
1210 return;
1211 }
1212
1213 let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
1214 cx.spawn(async move |cx| {
1215 let mut workspace_windows = cx.update(|cx| {
1216 cx.windows()
1217 .into_iter()
1218 .filter_map(|window| window.downcast::<Workspace>())
1219 .collect::<Vec<_>>()
1220 })?;
1221
1222 // If multiple windows have unsaved changes, and need a save prompt,
1223 // prompt in the active window before switching to a different window.
1224 cx.update(|cx| {
1225 workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false));
1226 })
1227 .log_err();
1228
1229 if should_confirm && let Some(workspace) = workspace_windows.first() {
1230 let answer = workspace
1231 .update(cx, |_, window, cx| {
1232 window.prompt(
1233 PromptLevel::Info,
1234 "Are you sure you want to quit?",
1235 None,
1236 &["Quit", "Cancel"],
1237 cx,
1238 )
1239 })
1240 .log_err();
1241
1242 if let Some(answer) = answer {
1243 WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release);
1244 let answer = answer.await.ok();
1245 WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release);
1246 if answer != Some(0) {
1247 return Ok(());
1248 }
1249 }
1250 }
1251
1252 // If the user cancels any save prompt, then keep the app open.
1253 for window in workspace_windows {
1254 if let Some(should_close) = window
1255 .update(cx, |workspace, window, cx| {
1256 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
1257 })
1258 .log_err()
1259 && !should_close.await?
1260 {
1261 return Ok(());
1262 }
1263 }
1264 cx.update(|cx| cx.quit())?;
1265 anyhow::Ok(())
1266 })
1267 .detach_and_log_err(cx);
1268}
1269
1270fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
1271 const MAX_LINES: usize = 1000;
1272 workspace
1273 .with_local_workspace(window, cx, move |workspace, window, cx| {
1274 let app_state = workspace.app_state();
1275 let languages = app_state.languages.clone();
1276 let fs = app_state.fs.clone();
1277 cx.spawn_in(window, async move |workspace, cx| {
1278 let (old_log, new_log, log_language) = futures::join!(
1279 fs.load(paths::old_log_file()),
1280 fs.load(paths::log_file()),
1281 languages.language_for_name("log")
1282 );
1283 let log = match (old_log, new_log) {
1284 (Err(_), Err(_)) => None,
1285 (old_log, new_log) => {
1286 let mut lines = VecDeque::with_capacity(MAX_LINES);
1287 for line in old_log
1288 .iter()
1289 .flat_map(|log| log.lines())
1290 .chain(new_log.iter().flat_map(|log| log.lines()))
1291 {
1292 if lines.len() == MAX_LINES {
1293 lines.pop_front();
1294 }
1295 lines.push_back(line);
1296 }
1297 Some(
1298 lines
1299 .into_iter()
1300 .flat_map(|line| [line, "\n"])
1301 .collect::<String>(),
1302 )
1303 }
1304 };
1305 let log_language = log_language.ok();
1306
1307 workspace
1308 .update_in(cx, |workspace, window, cx| {
1309 let Some(log) = log else {
1310 struct OpenLogError;
1311
1312 workspace.show_notification(
1313 NotificationId::unique::<OpenLogError>(),
1314 cx,
1315 |cx| {
1316 cx.new(|cx| {
1317 MessageNotification::new(
1318 format!(
1319 "Unable to access/open log file at path {:?}",
1320 paths::log_file().as_path()
1321 ),
1322 cx,
1323 )
1324 })
1325 },
1326 );
1327 return;
1328 };
1329 let project = workspace.project().clone();
1330 let buffer = project.update(cx, |project, cx| {
1331 project.create_local_buffer(&log, log_language, false, cx)
1332 });
1333
1334 let buffer = cx
1335 .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
1336 let editor = cx.new(|cx| {
1337 let mut editor =
1338 Editor::for_multibuffer(buffer, Some(project), window, cx);
1339 editor.set_read_only(true);
1340 editor.set_breadcrumb_header(format!(
1341 "Last {} lines in {}",
1342 MAX_LINES,
1343 paths::log_file().display()
1344 ));
1345 editor
1346 });
1347
1348 editor.update(cx, |editor, cx| {
1349 let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
1350 editor.change_selections(Default::default(), window, cx, |s| {
1351 s.select_ranges(Some(
1352 last_multi_buffer_offset..last_multi_buffer_offset,
1353 ));
1354 })
1355 });
1356
1357 workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
1358 })
1359 .log_err();
1360 })
1361 .detach();
1362 })
1363 .detach();
1364}
1365
1366pub fn handle_settings_file_changes(
1367 mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
1368 mut global_settings_file_rx: mpsc::UnboundedReceiver<String>,
1369 cx: &mut App,
1370 settings_changed: impl Fn(Option<anyhow::Error>, &mut App) + 'static,
1371) {
1372 MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx);
1373
1374 // Helper function to process settings content
1375 let process_settings = move |content: String,
1376 is_user: bool,
1377 store: &mut SettingsStore,
1378 cx: &mut App|
1379 -> bool {
1380 let result = if is_user {
1381 store.set_user_settings(&content, cx)
1382 } else {
1383 store.set_global_settings(&content, cx)
1384 };
1385
1386 let id = NotificationId::Named("failed-to-migrate-settings".into());
1387 // Apply migrations to both user and global settings
1388 let content_migrated = match result.migration_status {
1389 settings::MigrationStatus::Succeeded => {
1390 dismiss_app_notification(&id, cx);
1391 true
1392 }
1393 settings::MigrationStatus::NotNeeded => {
1394 dismiss_app_notification(&id, cx);
1395 false
1396 }
1397 settings::MigrationStatus::Failed { error: err } => {
1398 show_app_notification(id, cx, move |cx| {
1399 cx.new(|cx| {
1400 MessageNotification::new(
1401 format!(
1402 "Failed to migrate settings\n\
1403 {err}"
1404 ),
1405 cx,
1406 )
1407 .primary_message("Open Settings File")
1408 .primary_icon(IconName::Settings)
1409 .primary_on_click(|window, cx| {
1410 window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
1411 cx.emit(DismissEvent);
1412 })
1413 })
1414 });
1415 // notify user here
1416 false
1417 }
1418 };
1419
1420 if let settings::ParseStatus::Failed { error: err } = &result.parse_status {
1421 let settings_type = if is_user { "user" } else { "global" };
1422 log::error!("Failed to load {} settings: {err}", settings_type);
1423 }
1424
1425 settings_changed(
1426 match result.parse_status {
1427 settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)),
1428 settings::ParseStatus::Success => None,
1429 },
1430 cx,
1431 );
1432
1433 content_migrated
1434 };
1435
1436 // Initial load of both settings files
1437 let global_content = cx
1438 .background_executor()
1439 .block(global_settings_file_rx.next())
1440 .unwrap();
1441 let user_content = cx
1442 .background_executor()
1443 .block(user_settings_file_rx.next())
1444 .unwrap();
1445
1446 SettingsStore::update_global(cx, |store, cx| {
1447 process_settings(global_content, false, store, cx);
1448 process_settings(user_content, true, store, cx);
1449 });
1450
1451 // Watch for changes in both files
1452 cx.spawn(async move |cx| {
1453 let mut settings_streams = futures::stream::select(
1454 global_settings_file_rx.map(Either::Left),
1455 user_settings_file_rx.map(Either::Right),
1456 );
1457
1458 while let Some(content) = settings_streams.next().await {
1459 let (content, is_user) = match content {
1460 Either::Left(content) => (content, false),
1461 Either::Right(content) => (content, true),
1462 };
1463
1464 let result = cx.update_global(|store: &mut SettingsStore, cx| {
1465 let migrating_in_memory = process_settings(content, is_user, store, cx);
1466 if let Some(notifier) = MigrationNotification::try_global(cx) {
1467 notifier.update(cx, |_, cx| {
1468 cx.emit(MigrationEvent::ContentChanged {
1469 migration_type: MigrationType::Settings,
1470 migrating_in_memory,
1471 });
1472 });
1473 }
1474 cx.refresh_windows();
1475 });
1476
1477 if result.is_err() {
1478 break; // App dropped
1479 }
1480 }
1481 })
1482 .detach();
1483}
1484
1485pub fn handle_keymap_file_changes(
1486 mut user_keymap_file_rx: mpsc::UnboundedReceiver<String>,
1487 cx: &mut App,
1488) {
1489 let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded();
1490 let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded();
1491 let mut old_base_keymap = *BaseKeymap::get_global(cx);
1492 let mut old_vim_enabled = VimModeSetting::get_global(cx).0;
1493 let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1494
1495 cx.observe_global::<SettingsStore>(move |cx| {
1496 let new_base_keymap = *BaseKeymap::get_global(cx);
1497 let new_vim_enabled = VimModeSetting::get_global(cx).0;
1498 let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0;
1499
1500 if new_base_keymap != old_base_keymap
1501 || new_vim_enabled != old_vim_enabled
1502 || new_helix_enabled != old_helix_enabled
1503 {
1504 old_base_keymap = new_base_keymap;
1505 old_vim_enabled = new_vim_enabled;
1506 old_helix_enabled = new_helix_enabled;
1507
1508 base_keymap_tx.unbounded_send(()).unwrap();
1509 }
1510 })
1511 .detach();
1512
1513 #[cfg(target_os = "windows")]
1514 {
1515 let mut current_layout_id = cx.keyboard_layout().id().to_string();
1516 cx.on_keyboard_layout_change(move |cx| {
1517 let next_layout_id = cx.keyboard_layout().id();
1518 if next_layout_id != current_layout_id {
1519 current_layout_id = next_layout_id.to_string();
1520 keyboard_layout_tx.unbounded_send(()).ok();
1521 }
1522 })
1523 .detach();
1524 }
1525
1526 #[cfg(not(target_os = "windows"))]
1527 {
1528 let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned();
1529 cx.on_keyboard_layout_change(move |cx| {
1530 let next_mapping = cx.keyboard_mapper().get_key_equivalents();
1531 if current_mapping.as_ref() != next_mapping {
1532 current_mapping = next_mapping.cloned();
1533 keyboard_layout_tx.unbounded_send(()).ok();
1534 }
1535 })
1536 .detach();
1537 }
1538
1539 load_default_keymap(cx);
1540
1541 struct KeymapParseErrorNotification;
1542 let notification_id = NotificationId::unique::<KeymapParseErrorNotification>();
1543
1544 cx.spawn(async move |cx| {
1545 let mut user_keymap_content = String::new();
1546 let mut migrating_in_memory = false;
1547 loop {
1548 select_biased! {
1549 _ = base_keymap_rx.next() => {},
1550 _ = keyboard_layout_rx.next() => {},
1551 content = user_keymap_file_rx.next() => {
1552 if let Some(content) = content {
1553 if let Ok(Some(migrated_content)) = migrate_keymap(&content) {
1554 user_keymap_content = migrated_content;
1555 migrating_in_memory = true;
1556 } else {
1557 user_keymap_content = content;
1558 migrating_in_memory = false;
1559 }
1560 }
1561 }
1562 };
1563 cx.update(|cx| {
1564 if let Some(notifier) = MigrationNotification::try_global(cx) {
1565 notifier.update(cx, |_, cx| {
1566 cx.emit(MigrationEvent::ContentChanged {
1567 migration_type: MigrationType::Keymap,
1568 migrating_in_memory,
1569 });
1570 });
1571 }
1572 let load_result = KeymapFile::load(&user_keymap_content, cx);
1573 match load_result {
1574 KeymapFileLoadResult::Success { key_bindings } => {
1575 reload_keymaps(cx, key_bindings);
1576 dismiss_app_notification(¬ification_id.clone(), cx);
1577 }
1578 KeymapFileLoadResult::SomeFailedToLoad {
1579 key_bindings,
1580 error_message,
1581 } => {
1582 if !key_bindings.is_empty() {
1583 reload_keymaps(cx, key_bindings);
1584 }
1585 show_keymap_file_load_error(notification_id.clone(), error_message, cx);
1586 }
1587 KeymapFileLoadResult::JsonParseFailure { error } => {
1588 show_keymap_file_json_error(notification_id.clone(), &error, cx)
1589 }
1590 }
1591 })
1592 .ok();
1593 }
1594 })
1595 .detach();
1596}
1597
1598fn show_keymap_file_json_error(
1599 notification_id: NotificationId,
1600 error: &anyhow::Error,
1601 cx: &mut App,
1602) {
1603 let message: SharedString =
1604 format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into();
1605 show_app_notification(notification_id, cx, move |cx| {
1606 cx.new(|cx| {
1607 MessageNotification::new(message.clone(), cx)
1608 .primary_message("Open Keymap File")
1609 .primary_on_click(|window, cx| {
1610 window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
1611 cx.emit(DismissEvent);
1612 })
1613 })
1614 });
1615}
1616
1617fn show_keymap_file_load_error(
1618 notification_id: NotificationId,
1619 error_message: MarkdownString,
1620 cx: &mut App,
1621) {
1622 show_markdown_app_notification(
1623 notification_id,
1624 error_message,
1625 "Open Keymap File".into(),
1626 |window, cx| {
1627 window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
1628 cx.emit(DismissEvent);
1629 },
1630 cx,
1631 )
1632}
1633
1634fn show_markdown_app_notification<F>(
1635 notification_id: NotificationId,
1636 message: MarkdownString,
1637 primary_button_message: SharedString,
1638 primary_button_on_click: F,
1639 cx: &mut App,
1640) where
1641 F: 'static + Send + Sync + Fn(&mut Window, &mut Context<MessageNotification>),
1642{
1643 let parsed_markdown = cx.background_spawn(async move {
1644 let file_location_directory = None;
1645 let language_registry = None;
1646 markdown_preview::markdown_parser::parse_markdown(
1647 &message.0,
1648 file_location_directory,
1649 language_registry,
1650 )
1651 .await
1652 });
1653
1654 cx.spawn(async move |cx| {
1655 let parsed_markdown = Arc::new(parsed_markdown.await);
1656 let primary_button_message = primary_button_message.clone();
1657 let primary_button_on_click = Arc::new(primary_button_on_click);
1658 cx.update(|cx| {
1659 show_app_notification(notification_id, cx, move |cx| {
1660 let workspace_handle = cx.entity().downgrade();
1661 let parsed_markdown = parsed_markdown.clone();
1662 let primary_button_message = primary_button_message.clone();
1663 let primary_button_on_click = primary_button_on_click.clone();
1664 cx.new(move |cx| {
1665 MessageNotification::new_from_builder(cx, move |window, cx| {
1666 image_cache(retain_all("notification-cache"))
1667 .text_xs()
1668 .child(markdown_preview::markdown_renderer::render_parsed_markdown(
1669 &parsed_markdown.clone(),
1670 Some(workspace_handle.clone()),
1671 window,
1672 cx,
1673 ))
1674 .into_any()
1675 })
1676 .primary_message(primary_button_message)
1677 .primary_on_click_arc(primary_button_on_click)
1678 })
1679 })
1680 })
1681 .ok();
1682 })
1683 .detach();
1684}
1685
1686fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
1687 cx.clear_key_bindings();
1688 load_default_keymap(cx);
1689
1690 for key_binding in &mut user_key_bindings {
1691 key_binding.set_meta(KeybindSource::User.meta());
1692 }
1693 cx.bind_keys(user_key_bindings);
1694
1695 let menus = app_menus(cx);
1696 cx.set_menus(menus);
1697 // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`.
1698 #[cfg(not(target_os = "windows"))]
1699 cx.set_dock_menu(vec![gpui::MenuItem::action(
1700 "New Window",
1701 workspace::NewWindow,
1702 )]);
1703 // todo: nicer api here?
1704 keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx);
1705}
1706
1707pub fn load_default_keymap(cx: &mut App) {
1708 let base_keymap = *BaseKeymap::get_global(cx);
1709 if base_keymap == BaseKeymap::None {
1710 return;
1711 }
1712
1713 cx.bind_keys(
1714 KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(),
1715 );
1716
1717 if let Some(asset_path) = base_keymap.asset_path() {
1718 cx.bind_keys(KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap());
1719 }
1720
1721 if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 {
1722 cx.bind_keys(
1723 KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(),
1724 );
1725 }
1726}
1727
1728pub fn handle_settings_changed(error: Option<anyhow::Error>, cx: &mut App) {
1729 struct SettingsParseErrorNotification;
1730 let id = NotificationId::unique::<SettingsParseErrorNotification>();
1731
1732 match error {
1733 Some(error) => {
1734 if let Some(InvalidSettingsError::LocalSettings { .. }) =
1735 error.downcast_ref::<InvalidSettingsError>()
1736 {
1737 // Local settings errors are displayed by the projects
1738 return;
1739 }
1740 show_app_notification(id, cx, move |cx| {
1741 cx.new(|cx| {
1742 MessageNotification::new(format!("Invalid user settings file\n{error}"), cx)
1743 .primary_message("Open Settings File")
1744 .primary_icon(IconName::Settings)
1745 .primary_on_click(|window, cx| {
1746 window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx);
1747 cx.emit(DismissEvent);
1748 })
1749 })
1750 });
1751 }
1752 None => {
1753 dismiss_app_notification(&id, cx);
1754 }
1755 }
1756}
1757
1758pub fn open_new_ssh_project_from_project(
1759 workspace: &mut Workspace,
1760 paths: Vec<PathBuf>,
1761 window: &mut Window,
1762 cx: &mut Context<Workspace>,
1763) -> Task<anyhow::Result<()>> {
1764 let app_state = workspace.app_state().clone();
1765 let Some(ssh_client) = workspace.project().read(cx).remote_client() else {
1766 return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
1767 };
1768 let connection_options = ssh_client.read(cx).connection_options();
1769 cx.spawn_in(window, async move |_, cx| {
1770 open_remote_project(
1771 connection_options,
1772 paths,
1773 app_state,
1774 workspace::OpenOptions {
1775 open_new_workspace: Some(true),
1776 ..Default::default()
1777 },
1778 cx,
1779 )
1780 .await
1781 })
1782}
1783
1784fn open_project_settings_file(
1785 workspace: &mut Workspace,
1786 _: &OpenProjectSettingsFile,
1787 window: &mut Window,
1788 cx: &mut Context<Workspace>,
1789) {
1790 open_local_file(
1791 workspace,
1792 local_settings_file_relative_path(),
1793 initial_project_settings_content(),
1794 window,
1795 cx,
1796 )
1797}
1798
1799fn open_project_tasks_file(
1800 workspace: &mut Workspace,
1801 _: &OpenProjectTasks,
1802 window: &mut Window,
1803 cx: &mut Context<Workspace>,
1804) {
1805 open_local_file(
1806 workspace,
1807 local_tasks_file_relative_path(),
1808 initial_tasks_content(),
1809 window,
1810 cx,
1811 )
1812}
1813
1814fn open_project_debug_tasks_file(
1815 workspace: &mut Workspace,
1816 _: &zed_actions::OpenProjectDebugTasks,
1817 window: &mut Window,
1818 cx: &mut Context<Workspace>,
1819) {
1820 open_local_file(
1821 workspace,
1822 local_debug_file_relative_path(),
1823 initial_local_debug_tasks_content(),
1824 window,
1825 cx,
1826 )
1827}
1828
1829fn open_local_file(
1830 workspace: &mut Workspace,
1831 settings_relative_path: &'static RelPath,
1832 initial_contents: Cow<'static, str>,
1833 window: &mut Window,
1834 cx: &mut Context<Workspace>,
1835) {
1836 let project = workspace.project().clone();
1837 let worktree = project
1838 .read(cx)
1839 .visible_worktrees(cx)
1840 .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
1841 if let Some(worktree) = worktree {
1842 let tree_id = worktree.read(cx).id();
1843 cx.spawn_in(window, async move |workspace, cx| {
1844 // Check if the file actually exists on disk (even if it's excluded from worktree)
1845 let file_exists = {
1846 let full_path = worktree.read_with(cx, |tree, _| {
1847 tree.abs_path().join(settings_relative_path.as_std_path())
1848 })?;
1849
1850 let fs = project.read_with(cx, |project, _| project.fs().clone())?;
1851
1852 fs.metadata(&full_path)
1853 .await
1854 .ok()
1855 .flatten()
1856 .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo)
1857 };
1858
1859 if !file_exists {
1860 if let Some(dir_path) = settings_relative_path.parent()
1861 && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())?
1862 {
1863 project
1864 .update(cx, |project, cx| {
1865 project.create_entry((tree_id, dir_path), true, cx)
1866 })?
1867 .await
1868 .context("worktree was removed")?;
1869 }
1870
1871 if worktree.read_with(cx, |tree, _| {
1872 tree.entry_for_path(settings_relative_path).is_none()
1873 })? {
1874 project
1875 .update(cx, |project, cx| {
1876 project.create_entry((tree_id, settings_relative_path), false, cx)
1877 })?
1878 .await
1879 .context("worktree was removed")?;
1880 }
1881 }
1882
1883 let editor = workspace
1884 .update_in(cx, |workspace, window, cx| {
1885 workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1886 })?
1887 .await?
1888 .downcast::<Editor>()
1889 .context("unexpected item type: expected editor item")?;
1890
1891 editor
1892 .downgrade()
1893 .update(cx, |editor, cx| {
1894 if let Some(buffer) = editor.buffer().read(cx).as_singleton()
1895 && buffer.read(cx).is_empty()
1896 {
1897 buffer.update(cx, |buffer, cx| {
1898 buffer.edit([(0..0, initial_contents)], None, cx)
1899 });
1900 }
1901 })
1902 .ok();
1903
1904 anyhow::Ok(())
1905 })
1906 .detach();
1907 } else {
1908 struct NoOpenFolders;
1909
1910 workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1911 cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1912 })
1913 }
1914}
1915
1916fn open_telemetry_log_file(
1917 workspace: &mut Workspace,
1918 window: &mut Window,
1919 cx: &mut Context<Workspace>,
1920) {
1921 const HEADER: &str = concat!(
1922 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1923 "// Telemetry can be disabled via the `settings.json` file.\n",
1924 "// Here is the data that has been reported for the current session:\n",
1925 );
1926 workspace
1927 .with_local_workspace(window, cx, move |workspace, window, cx| {
1928 let app_state = workspace.app_state().clone();
1929 cx.spawn_in(window, async move |workspace, cx| {
1930 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
1931 let path = client::telemetry::Telemetry::log_file_path();
1932 app_state.fs.load(&path).await.log_err()
1933 }
1934
1935 let log = fetch_log_string(&app_state)
1936 .await
1937 .unwrap_or_else(|| "// No data has been collected yet".to_string());
1938
1939 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
1940 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
1941 if let Some(newline_offset) = log[start_offset..].find('\n') {
1942 start_offset += newline_offset + 1;
1943 }
1944 let log_suffix = &log[start_offset..];
1945 let content = format!("{}\n{}", HEADER, log_suffix);
1946 let json = app_state
1947 .languages
1948 .language_for_name("JSON")
1949 .await
1950 .log_err();
1951
1952 workspace
1953 .update_in(cx, |workspace, window, cx| {
1954 let project = workspace.project().clone();
1955 let buffer = project.update(cx, |project, cx| {
1956 project.create_local_buffer(&content, json, false, cx)
1957 });
1958 let buffer = cx.new(|cx| {
1959 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
1960 });
1961 workspace.add_item_to_active_pane(
1962 Box::new(cx.new(|cx| {
1963 let mut editor =
1964 Editor::for_multibuffer(buffer, Some(project), window, cx);
1965 editor.set_read_only(true);
1966 editor.set_breadcrumb_header("Telemetry Log".into());
1967 editor
1968 })),
1969 None,
1970 true,
1971 window,
1972 cx,
1973 );
1974 })
1975 .log_err()?;
1976
1977 Some(())
1978 })
1979 .detach();
1980 })
1981 .detach();
1982}
1983
1984fn open_bundled_file(
1985 workspace: &Workspace,
1986 text: Cow<'static, str>,
1987 title: &'static str,
1988 language: &'static str,
1989 window: &mut Window,
1990 cx: &mut Context<Workspace>,
1991) {
1992 let language = workspace.app_state().languages.language_for_name(language);
1993 cx.spawn_in(window, async move |workspace, cx| {
1994 let language = language.await.log_err();
1995 workspace
1996 .update_in(cx, |workspace, window, cx| {
1997 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
1998 let project = workspace.project();
1999 let buffer = project.update(cx, move |project, cx| {
2000 let buffer =
2001 project.create_local_buffer(text.as_ref(), language, false, cx);
2002 buffer.update(cx, |buffer, cx| {
2003 buffer.set_capability(Capability::ReadOnly, cx);
2004 });
2005 buffer
2006 });
2007 let buffer =
2008 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
2009 workspace.add_item_to_active_pane(
2010 Box::new(cx.new(|cx| {
2011 let mut editor =
2012 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2013 editor.set_read_only(true);
2014 editor.set_should_serialize(false, cx);
2015 editor.set_breadcrumb_header(title.into());
2016 editor
2017 })),
2018 None,
2019 true,
2020 window,
2021 cx,
2022 );
2023 })
2024 })?
2025 .await
2026 })
2027 .detach_and_log_err(cx);
2028}
2029
2030fn open_settings_file(
2031 abs_path: &'static Path,
2032 default_content: impl FnOnce() -> Rope + Send + 'static,
2033 window: &mut Window,
2034 cx: &mut Context<Workspace>,
2035) {
2036 cx.spawn_in(window, async move |workspace, cx| {
2037 let (worktree_creation_task, settings_open_task) = workspace
2038 .update_in(cx, |workspace, window, cx| {
2039 workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
2040 let worktree_creation_task = workspace.project().update(cx, |project, cx| {
2041 // Set up a dedicated worktree for settings, since
2042 // otherwise we're dropping and re-starting LSP servers
2043 // for each file inside on every settings file
2044 // close/open
2045
2046 // TODO: Do note that all other external files (e.g.
2047 // drag and drop from OS) still have their worktrees
2048 // released on file close, causing LSP servers'
2049 // restarts.
2050 project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
2051 });
2052 let settings_open_task =
2053 create_and_open_local_file(abs_path, window, cx, default_content);
2054 (worktree_creation_task, settings_open_task)
2055 })
2056 })?
2057 .await?;
2058 let _ = worktree_creation_task.await?;
2059 let _ = settings_open_task.await?;
2060 anyhow::Ok(())
2061 })
2062 .detach_and_log_err(cx);
2063}
2064
2065fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
2066 struct CaptureRecentAudioNotification {
2067 focus_handle: gpui::FocusHandle,
2068 save_result: Option<Result<(PathBuf, Duration), anyhow::Error>>,
2069 _save_task: Task<anyhow::Result<()>>,
2070 }
2071
2072 impl gpui::EventEmitter<DismissEvent> for CaptureRecentAudioNotification {}
2073 impl gpui::EventEmitter<SuppressEvent> for CaptureRecentAudioNotification {}
2074 impl gpui::Focusable for CaptureRecentAudioNotification {
2075 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
2076 self.focus_handle.clone()
2077 }
2078 }
2079 impl workspace::notifications::Notification for CaptureRecentAudioNotification {}
2080
2081 impl Render for CaptureRecentAudioNotification {
2082 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2083 let message = match &self.save_result {
2084 None => format!(
2085 "Saving up to {} seconds of recent audio",
2086 REPLAY_DURATION.as_secs(),
2087 ),
2088 Some(Ok((path, duration))) => format!(
2089 "Saved {} seconds of all audio to {}",
2090 duration.as_secs(),
2091 path.display(),
2092 ),
2093 Some(Err(e)) => format!("Error saving audio replays: {e:?}"),
2094 };
2095
2096 NotificationFrame::new()
2097 .with_title(Some("Saved Audio"))
2098 .show_suppress_button(false)
2099 .on_close(cx.listener(|_, _, _, cx| {
2100 cx.emit(DismissEvent);
2101 }))
2102 .with_content(message)
2103 }
2104 }
2105
2106 impl CaptureRecentAudioNotification {
2107 fn new(cx: &mut Context<Self>) -> Self {
2108 if AudioSettings::get_global(cx).rodio_audio {
2109 let executor = cx.background_executor().clone();
2110 let save_task = cx.default_global::<audio::Audio>().save_replays(executor);
2111 let _save_task = cx.spawn(async move |this, cx| {
2112 let res = save_task.await;
2113 this.update(cx, |this, cx| {
2114 this.save_result = Some(res);
2115 cx.notify();
2116 })
2117 });
2118
2119 Self {
2120 focus_handle: cx.focus_handle(),
2121 _save_task,
2122 save_result: None,
2123 }
2124 } else {
2125 Self {
2126 focus_handle: cx.focus_handle(),
2127 _save_task: Task::ready(Ok(())),
2128 save_result: Some(Err(anyhow::anyhow!(
2129 "Capturing recent audio is only supported on the experimental rodio audio pipeline"
2130 ))),
2131 }
2132 }
2133 }
2134 }
2135
2136 workspace.show_notification(
2137 NotificationId::unique::<CaptureRecentAudioNotification>(),
2138 cx,
2139 |cx| cx.new(CaptureRecentAudioNotification::new),
2140 );
2141}
2142
2143/// Eagerly loads the active theme and icon theme based on the selections in the
2144/// theme settings.
2145///
2146/// This fast path exists to load these themes as soon as possible so the user
2147/// doesn't see the default themes while waiting on extensions to load.
2148pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc<dyn Fs>, cx: &mut App) {
2149 let extension_store = ExtensionStore::global(cx);
2150 let theme_registry = ThemeRegistry::global(cx);
2151 let theme_settings = ThemeSettings::get_global(cx);
2152 let appearance = SystemAppearance::global(cx).0;
2153
2154 enum LoadTarget {
2155 Theme(PathBuf),
2156 IconTheme((PathBuf, PathBuf)),
2157 }
2158
2159 let theme_name = theme_settings.theme.name(appearance);
2160 let icon_theme_name = theme_settings.icon_theme.name(appearance);
2161 let themes_to_load = [
2162 theme_registry
2163 .get(&theme_name.0)
2164 .is_err()
2165 .then(|| {
2166 extension_store
2167 .read(cx)
2168 .path_to_extension_theme(&theme_name.0)
2169 })
2170 .flatten()
2171 .map(LoadTarget::Theme),
2172 theme_registry
2173 .get_icon_theme(&icon_theme_name.0)
2174 .is_err()
2175 .then(|| {
2176 extension_store
2177 .read(cx)
2178 .path_to_extension_icon_theme(&icon_theme_name.0)
2179 })
2180 .flatten()
2181 .map(LoadTarget::IconTheme),
2182 ];
2183
2184 enum ReloadTarget {
2185 Theme,
2186 IconTheme,
2187 }
2188
2189 let executor = cx.background_executor();
2190 let reload_tasks = parking_lot::Mutex::new(Vec::with_capacity(themes_to_load.len()));
2191
2192 let mut themes_to_load = themes_to_load.into_iter().flatten().peekable();
2193
2194 if themes_to_load.peek().is_none() {
2195 return;
2196 }
2197
2198 executor.block(executor.scoped(|scope| {
2199 for load_target in themes_to_load {
2200 let theme_registry = &theme_registry;
2201 let reload_tasks = &reload_tasks;
2202 let fs = fs.clone();
2203
2204 scope.spawn(async {
2205 match load_target {
2206 LoadTarget::Theme(theme_path) => {
2207 if theme_registry
2208 .load_user_theme(&theme_path, fs)
2209 .await
2210 .log_err()
2211 .is_some()
2212 {
2213 reload_tasks.lock().push(ReloadTarget::Theme);
2214 }
2215 }
2216 LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => {
2217 if theme_registry
2218 .load_icon_theme(&icon_theme_path, &icons_root_path, fs)
2219 .await
2220 .log_err()
2221 .is_some()
2222 {
2223 reload_tasks.lock().push(ReloadTarget::IconTheme);
2224 }
2225 }
2226 }
2227 });
2228 }
2229 }));
2230
2231 for reload_target in reload_tasks.into_inner() {
2232 match reload_target {
2233 ReloadTarget::Theme => GlobalTheme::reload_theme(cx),
2234 ReloadTarget::IconTheme => GlobalTheme::reload_icon_theme(cx),
2235 };
2236 }
2237}
2238
2239#[cfg(test)]
2240mod tests {
2241 use super::*;
2242 use assets::Assets;
2243 use collections::HashSet;
2244 use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow};
2245 use gpui::{
2246 Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
2247 TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
2248 };
2249 use language::{LanguageMatcher, LanguageRegistry};
2250 use pretty_assertions::{assert_eq, assert_ne};
2251 use project::{Project, ProjectPath};
2252 use serde_json::json;
2253 use settings::{SettingsStore, watch_config_file};
2254 use std::{
2255 path::{Path, PathBuf},
2256 time::Duration,
2257 };
2258 use theme::ThemeRegistry;
2259 use util::{
2260 path,
2261 rel_path::{RelPath, rel_path},
2262 };
2263 use workspace::{
2264 NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
2265 WorkspaceHandle,
2266 item::SaveOptions,
2267 item::{Item, ItemHandle},
2268 open_new, open_paths, pane,
2269 };
2270
2271 #[gpui::test]
2272 async fn test_open_non_existing_file(cx: &mut TestAppContext) {
2273 let app_state = init_test(cx);
2274 app_state
2275 .fs
2276 .as_fake()
2277 .insert_tree(
2278 path!("/root"),
2279 json!({
2280 "a": {
2281 },
2282 }),
2283 )
2284 .await;
2285
2286 cx.update(|cx| {
2287 open_paths(
2288 &[PathBuf::from(path!("/root/a/new"))],
2289 app_state.clone(),
2290 workspace::OpenOptions::default(),
2291 cx,
2292 )
2293 })
2294 .await
2295 .unwrap();
2296 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2297
2298 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
2299 workspace
2300 .update(cx, |workspace, _, cx| {
2301 assert!(workspace.active_item_as::<Editor>(cx).is_some())
2302 })
2303 .unwrap();
2304 }
2305
2306 #[gpui::test]
2307 async fn test_open_paths_action(cx: &mut TestAppContext) {
2308 let app_state = init_test(cx);
2309 app_state
2310 .fs
2311 .as_fake()
2312 .insert_tree(
2313 "/root",
2314 json!({
2315 "a": {
2316 "aa": null,
2317 "ab": null,
2318 },
2319 "b": {
2320 "ba": null,
2321 "bb": null,
2322 },
2323 "c": {
2324 "ca": null,
2325 "cb": null,
2326 },
2327 "d": {
2328 "da": null,
2329 "db": null,
2330 },
2331 "e": {
2332 "ea": null,
2333 "eb": null,
2334 }
2335 }),
2336 )
2337 .await;
2338
2339 cx.update(|cx| {
2340 open_paths(
2341 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
2342 app_state.clone(),
2343 workspace::OpenOptions::default(),
2344 cx,
2345 )
2346 })
2347 .await
2348 .unwrap();
2349 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2350
2351 cx.update(|cx| {
2352 open_paths(
2353 &[PathBuf::from("/root/a")],
2354 app_state.clone(),
2355 workspace::OpenOptions::default(),
2356 cx,
2357 )
2358 })
2359 .await
2360 .unwrap();
2361 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2362 let workspace_1 = cx
2363 .read(|cx| cx.windows()[0].downcast::<Workspace>())
2364 .unwrap();
2365 cx.run_until_parked();
2366 workspace_1
2367 .update(cx, |workspace, window, cx| {
2368 assert_eq!(workspace.worktrees(cx).count(), 2);
2369 assert!(workspace.left_dock().read(cx).is_open());
2370 assert!(
2371 workspace
2372 .active_pane()
2373 .read(cx)
2374 .focus_handle(cx)
2375 .is_focused(window)
2376 );
2377 })
2378 .unwrap();
2379
2380 cx.update(|cx| {
2381 open_paths(
2382 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
2383 app_state.clone(),
2384 workspace::OpenOptions::default(),
2385 cx,
2386 )
2387 })
2388 .await
2389 .unwrap();
2390 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2391
2392 // Replace existing windows
2393 let window = cx
2394 .update(|cx| cx.windows()[0].downcast::<Workspace>())
2395 .unwrap();
2396 cx.update(|cx| {
2397 open_paths(
2398 &[PathBuf::from("/root/e")],
2399 app_state,
2400 workspace::OpenOptions {
2401 replace_window: Some(window),
2402 ..Default::default()
2403 },
2404 cx,
2405 )
2406 })
2407 .await
2408 .unwrap();
2409 cx.background_executor.run_until_parked();
2410 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2411 let workspace_1 = cx
2412 .update(|cx| cx.windows()[0].downcast::<Workspace>())
2413 .unwrap();
2414 workspace_1
2415 .update(cx, |workspace, window, cx| {
2416 assert_eq!(
2417 workspace
2418 .worktrees(cx)
2419 .map(|w| w.read(cx).abs_path())
2420 .collect::<Vec<_>>(),
2421 &[Path::new("/root/e").into()]
2422 );
2423 assert!(workspace.left_dock().read(cx).is_open());
2424 assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
2425 })
2426 .unwrap();
2427 }
2428
2429 #[gpui::test]
2430 async fn test_open_add_new(cx: &mut TestAppContext) {
2431 let app_state = init_test(cx);
2432 app_state
2433 .fs
2434 .as_fake()
2435 .insert_tree(
2436 path!("/root"),
2437 json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
2438 )
2439 .await;
2440
2441 cx.update(|cx| {
2442 open_paths(
2443 &[PathBuf::from(path!("/root/dir"))],
2444 app_state.clone(),
2445 workspace::OpenOptions::default(),
2446 cx,
2447 )
2448 })
2449 .await
2450 .unwrap();
2451 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2452
2453 cx.update(|cx| {
2454 open_paths(
2455 &[PathBuf::from(path!("/root/a"))],
2456 app_state.clone(),
2457 workspace::OpenOptions {
2458 open_new_workspace: Some(false),
2459 ..Default::default()
2460 },
2461 cx,
2462 )
2463 })
2464 .await
2465 .unwrap();
2466 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2467
2468 cx.update(|cx| {
2469 open_paths(
2470 &[PathBuf::from(path!("/root/dir/c"))],
2471 app_state.clone(),
2472 workspace::OpenOptions {
2473 open_new_workspace: Some(true),
2474 ..Default::default()
2475 },
2476 cx,
2477 )
2478 })
2479 .await
2480 .unwrap();
2481 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2482 }
2483
2484 #[gpui::test]
2485 async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
2486 let app_state = init_test(cx);
2487 app_state
2488 .fs
2489 .as_fake()
2490 .insert_tree(
2491 path!("/root"),
2492 json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
2493 )
2494 .await;
2495
2496 cx.update(|cx| {
2497 open_paths(
2498 &[PathBuf::from(path!("/root/dir1/a"))],
2499 app_state.clone(),
2500 workspace::OpenOptions::default(),
2501 cx,
2502 )
2503 })
2504 .await
2505 .unwrap();
2506 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2507 let window1 = cx.update(|cx| cx.active_window().unwrap());
2508
2509 cx.update(|cx| {
2510 open_paths(
2511 &[PathBuf::from(path!("/root/dir2/c"))],
2512 app_state.clone(),
2513 workspace::OpenOptions::default(),
2514 cx,
2515 )
2516 })
2517 .await
2518 .unwrap();
2519 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2520
2521 cx.update(|cx| {
2522 open_paths(
2523 &[PathBuf::from(path!("/root/dir2"))],
2524 app_state.clone(),
2525 workspace::OpenOptions::default(),
2526 cx,
2527 )
2528 })
2529 .await
2530 .unwrap();
2531 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2532 let window2 = cx.update(|cx| cx.active_window().unwrap());
2533 assert!(window1 != window2);
2534 cx.update_window(window1, |_, window, _| window.activate_window())
2535 .unwrap();
2536
2537 cx.update(|cx| {
2538 open_paths(
2539 &[PathBuf::from(path!("/root/dir2/c"))],
2540 app_state.clone(),
2541 workspace::OpenOptions::default(),
2542 cx,
2543 )
2544 })
2545 .await
2546 .unwrap();
2547 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2548 // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2549 assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2550 }
2551
2552 #[gpui::test]
2553 async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2554 let executor = cx.executor();
2555 let app_state = init_test(cx);
2556
2557 cx.update(|cx| {
2558 SettingsStore::update_global(cx, |store, cx| {
2559 store.update_user_settings(cx, |settings| {
2560 settings
2561 .session
2562 .get_or_insert_default()
2563 .restore_unsaved_buffers = Some(false)
2564 });
2565 });
2566 });
2567
2568 app_state
2569 .fs
2570 .as_fake()
2571 .insert_tree(path!("/root"), json!({"a": "hey"}))
2572 .await;
2573
2574 cx.update(|cx| {
2575 open_paths(
2576 &[PathBuf::from(path!("/root/a"))],
2577 app_state.clone(),
2578 workspace::OpenOptions::default(),
2579 cx,
2580 )
2581 })
2582 .await
2583 .unwrap();
2584 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2585
2586 // When opening the workspace, the window is not in a edited state.
2587 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2588
2589 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2590 cx.update(|cx| window.read(cx).unwrap().is_edited())
2591 };
2592 let pane = window
2593 .read_with(cx, |workspace, _| workspace.active_pane().clone())
2594 .unwrap();
2595 let editor = window
2596 .read_with(cx, |workspace, cx| {
2597 workspace
2598 .active_item(cx)
2599 .unwrap()
2600 .downcast::<Editor>()
2601 .unwrap()
2602 })
2603 .unwrap();
2604
2605 assert!(!window_is_edited(window, cx));
2606
2607 // Editing a buffer marks the window as edited.
2608 window
2609 .update(cx, |_, window, cx| {
2610 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2611 })
2612 .unwrap();
2613
2614 assert!(window_is_edited(window, cx));
2615
2616 // Undoing the edit restores the window's edited state.
2617 window
2618 .update(cx, |_, window, cx| {
2619 editor.update(cx, |editor, cx| {
2620 editor.undo(&Default::default(), window, cx)
2621 });
2622 })
2623 .unwrap();
2624 assert!(!window_is_edited(window, cx));
2625
2626 // Redoing the edit marks the window as edited again.
2627 window
2628 .update(cx, |_, window, cx| {
2629 editor.update(cx, |editor, cx| {
2630 editor.redo(&Default::default(), window, cx)
2631 });
2632 })
2633 .unwrap();
2634 assert!(window_is_edited(window, cx));
2635 let weak = editor.downgrade();
2636
2637 // Closing the item restores the window's edited state.
2638 let close = window
2639 .update(cx, |_, window, cx| {
2640 pane.update(cx, |pane, cx| {
2641 drop(editor);
2642 pane.close_active_item(&Default::default(), window, cx)
2643 })
2644 })
2645 .unwrap();
2646 executor.run_until_parked();
2647
2648 cx.simulate_prompt_answer("Don't Save");
2649 close.await.unwrap();
2650
2651 // Advance the clock to ensure that the item has been serialized and dropped from the queue
2652 cx.executor().advance_clock(Duration::from_secs(1));
2653
2654 weak.assert_released();
2655 assert!(!window_is_edited(window, cx));
2656 // Opening the buffer again doesn't impact the window's edited state.
2657 cx.update(|cx| {
2658 open_paths(
2659 &[PathBuf::from(path!("/root/a"))],
2660 app_state,
2661 workspace::OpenOptions::default(),
2662 cx,
2663 )
2664 })
2665 .await
2666 .unwrap();
2667 executor.run_until_parked();
2668
2669 window
2670 .update(cx, |workspace, _, cx| {
2671 let editor = workspace
2672 .active_item(cx)
2673 .unwrap()
2674 .downcast::<Editor>()
2675 .unwrap();
2676
2677 editor.update(cx, |editor, cx| {
2678 assert_eq!(editor.text(cx), "hey");
2679 });
2680 })
2681 .unwrap();
2682
2683 let editor = window
2684 .read_with(cx, |workspace, cx| {
2685 workspace
2686 .active_item(cx)
2687 .unwrap()
2688 .downcast::<Editor>()
2689 .unwrap()
2690 })
2691 .unwrap();
2692 assert!(!window_is_edited(window, cx));
2693
2694 // Editing the buffer marks the window as edited.
2695 window
2696 .update(cx, |_, window, cx| {
2697 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2698 })
2699 .unwrap();
2700 executor.run_until_parked();
2701 assert!(window_is_edited(window, cx));
2702
2703 // Ensure closing the window via the mouse gets preempted due to the
2704 // buffer having unsaved changes.
2705 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2706 executor.run_until_parked();
2707 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2708
2709 // The window is successfully closed after the user dismisses the prompt.
2710 cx.simulate_prompt_answer("Don't Save");
2711 executor.run_until_parked();
2712 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2713 }
2714
2715 #[gpui::test]
2716 async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2717 let app_state = init_test(cx);
2718 app_state
2719 .fs
2720 .as_fake()
2721 .insert_tree(path!("/root"), json!({"a": "hey"}))
2722 .await;
2723
2724 cx.update(|cx| {
2725 open_paths(
2726 &[PathBuf::from(path!("/root/a"))],
2727 app_state.clone(),
2728 workspace::OpenOptions::default(),
2729 cx,
2730 )
2731 })
2732 .await
2733 .unwrap();
2734
2735 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2736
2737 // When opening the workspace, the window is not in a edited state.
2738 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2739
2740 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2741 cx.update(|cx| window.read(cx).unwrap().is_edited())
2742 };
2743
2744 let editor = window
2745 .read_with(cx, |workspace, cx| {
2746 workspace
2747 .active_item(cx)
2748 .unwrap()
2749 .downcast::<Editor>()
2750 .unwrap()
2751 })
2752 .unwrap();
2753
2754 assert!(!window_is_edited(window, cx));
2755
2756 // Editing a buffer marks the window as edited.
2757 window
2758 .update(cx, |_, window, cx| {
2759 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2760 })
2761 .unwrap();
2762
2763 assert!(window_is_edited(window, cx));
2764 cx.run_until_parked();
2765
2766 // Advance the clock to make sure the workspace is serialized
2767 cx.executor().advance_clock(Duration::from_secs(1));
2768
2769 // When closing the window, no prompt shows up and the window is closed.
2770 // buffer having unsaved changes.
2771 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2772 cx.run_until_parked();
2773 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2774
2775 // When we now reopen the window, the edited state and the edited buffer are back
2776 cx.update(|cx| {
2777 open_paths(
2778 &[PathBuf::from(path!("/root/a"))],
2779 app_state.clone(),
2780 workspace::OpenOptions::default(),
2781 cx,
2782 )
2783 })
2784 .await
2785 .unwrap();
2786
2787 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2788 assert!(cx.update(|cx| cx.active_window().is_some()));
2789
2790 cx.run_until_parked();
2791
2792 // When opening the workspace, the window is not in a edited state.
2793 let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2794 assert!(window_is_edited(window, cx));
2795
2796 window
2797 .update(cx, |workspace, _, cx| {
2798 let editor = workspace
2799 .active_item(cx)
2800 .unwrap()
2801 .downcast::<editor::Editor>()
2802 .unwrap();
2803 editor.update(cx, |editor, cx| {
2804 assert_eq!(editor.text(cx), "EDIThey");
2805 assert!(editor.is_dirty(cx));
2806 });
2807
2808 editor
2809 })
2810 .unwrap();
2811 }
2812
2813 #[gpui::test]
2814 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2815 let app_state = init_test(cx);
2816 cx.update(|cx| {
2817 open_new(
2818 Default::default(),
2819 app_state.clone(),
2820 cx,
2821 |workspace, window, cx| {
2822 Editor::new_file(workspace, &Default::default(), window, cx)
2823 },
2824 )
2825 })
2826 .await
2827 .unwrap();
2828 cx.run_until_parked();
2829
2830 let workspace = cx
2831 .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2832 .unwrap();
2833
2834 let editor = workspace
2835 .update(cx, |workspace, _, cx| {
2836 let editor = workspace
2837 .active_item(cx)
2838 .unwrap()
2839 .downcast::<editor::Editor>()
2840 .unwrap();
2841 editor.update(cx, |editor, cx| {
2842 assert!(editor.text(cx).is_empty());
2843 assert!(!editor.is_dirty(cx));
2844 });
2845
2846 editor
2847 })
2848 .unwrap();
2849
2850 let save_task = workspace
2851 .update(cx, |workspace, window, cx| {
2852 workspace.save_active_item(SaveIntent::Save, window, cx)
2853 })
2854 .unwrap();
2855 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2856 cx.background_executor.run_until_parked();
2857 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2858 save_task.await.unwrap();
2859 workspace
2860 .update(cx, |_, _, cx| {
2861 editor.update(cx, |editor, cx| {
2862 assert!(!editor.is_dirty(cx));
2863 assert_eq!(editor.title(cx), "the-new-name");
2864 });
2865 })
2866 .unwrap();
2867 }
2868
2869 #[gpui::test]
2870 async fn test_open_entry(cx: &mut TestAppContext) {
2871 let app_state = init_test(cx);
2872 app_state
2873 .fs
2874 .as_fake()
2875 .insert_tree(
2876 path!("/root"),
2877 json!({
2878 "a": {
2879 "file1": "contents 1",
2880 "file2": "contents 2",
2881 "file3": "contents 3",
2882 },
2883 }),
2884 )
2885 .await;
2886
2887 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2888 project.update(cx, |project, _cx| {
2889 project.languages().add(markdown_language())
2890 });
2891 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2892 let workspace = window.root(cx).unwrap();
2893
2894 let entries = cx.read(|cx| workspace.file_project_paths(cx));
2895 let file1 = entries[0].clone();
2896 let file2 = entries[1].clone();
2897 let file3 = entries[2].clone();
2898
2899 // Open the first entry
2900 let entry_1 = window
2901 .update(cx, |w, window, cx| {
2902 w.open_path(file1.clone(), None, true, window, cx)
2903 })
2904 .unwrap()
2905 .await
2906 .unwrap();
2907 cx.read(|cx| {
2908 let pane = workspace.read(cx).active_pane().read(cx);
2909 assert_eq!(
2910 pane.active_item().unwrap().project_path(cx),
2911 Some(file1.clone())
2912 );
2913 assert_eq!(pane.items_len(), 1);
2914 });
2915
2916 // Open the second entry
2917 window
2918 .update(cx, |w, window, cx| {
2919 w.open_path(file2.clone(), None, true, window, cx)
2920 })
2921 .unwrap()
2922 .await
2923 .unwrap();
2924 cx.read(|cx| {
2925 let pane = workspace.read(cx).active_pane().read(cx);
2926 assert_eq!(
2927 pane.active_item().unwrap().project_path(cx),
2928 Some(file2.clone())
2929 );
2930 assert_eq!(pane.items_len(), 2);
2931 });
2932
2933 // Open the first entry again. The existing pane item is activated.
2934 let entry_1b = window
2935 .update(cx, |w, window, cx| {
2936 w.open_path(file1.clone(), None, true, window, cx)
2937 })
2938 .unwrap()
2939 .await
2940 .unwrap();
2941 assert_eq!(entry_1.item_id(), entry_1b.item_id());
2942
2943 cx.read(|cx| {
2944 let pane = workspace.read(cx).active_pane().read(cx);
2945 assert_eq!(
2946 pane.active_item().unwrap().project_path(cx),
2947 Some(file1.clone())
2948 );
2949 assert_eq!(pane.items_len(), 2);
2950 });
2951
2952 // Split the pane with the first entry, then open the second entry again.
2953 window
2954 .update(cx, |w, window, cx| {
2955 w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx)
2956 })
2957 .unwrap()
2958 .await
2959 .unwrap();
2960 window
2961 .update(cx, |w, window, cx| {
2962 w.open_path(file2.clone(), None, true, window, cx)
2963 })
2964 .unwrap()
2965 .await
2966 .unwrap();
2967
2968 window
2969 .read_with(cx, |w, cx| {
2970 assert_eq!(
2971 w.active_pane()
2972 .read(cx)
2973 .active_item()
2974 .unwrap()
2975 .project_path(cx),
2976 Some(file2.clone())
2977 );
2978 })
2979 .unwrap();
2980
2981 // Open the third entry twice concurrently. Only one pane item is added.
2982 let (t1, t2) = window
2983 .update(cx, |w, window, cx| {
2984 (
2985 w.open_path(file3.clone(), None, true, window, cx),
2986 w.open_path(file3.clone(), None, true, window, cx),
2987 )
2988 })
2989 .unwrap();
2990 t1.await.unwrap();
2991 t2.await.unwrap();
2992 cx.read(|cx| {
2993 let pane = workspace.read(cx).active_pane().read(cx);
2994 assert_eq!(
2995 pane.active_item().unwrap().project_path(cx),
2996 Some(file3.clone())
2997 );
2998 let pane_entries = pane
2999 .items()
3000 .map(|i| i.project_path(cx).unwrap())
3001 .collect::<Vec<_>>();
3002 assert_eq!(pane_entries, &[file1, file2, file3]);
3003 });
3004 }
3005
3006 #[gpui::test]
3007 async fn test_open_paths(cx: &mut TestAppContext) {
3008 let app_state = init_test(cx);
3009
3010 app_state
3011 .fs
3012 .as_fake()
3013 .insert_tree(
3014 path!("/"),
3015 json!({
3016 "dir1": {
3017 "a.txt": ""
3018 },
3019 "dir2": {
3020 "b.txt": ""
3021 },
3022 "dir3": {
3023 "c.txt": ""
3024 },
3025 "d.txt": ""
3026 }),
3027 )
3028 .await;
3029
3030 cx.update(|cx| {
3031 open_paths(
3032 &[PathBuf::from(path!("/dir1/"))],
3033 app_state,
3034 workspace::OpenOptions::default(),
3035 cx,
3036 )
3037 })
3038 .await
3039 .unwrap();
3040 cx.run_until_parked();
3041 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
3042 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
3043 let workspace = window.root(cx).unwrap();
3044
3045 #[track_caller]
3046 fn assert_project_panel_selection(
3047 workspace: &Workspace,
3048 expected_worktree_path: &Path,
3049 expected_entry_path: &RelPath,
3050 cx: &App,
3051 ) {
3052 let project_panel = [
3053 workspace.left_dock().read(cx).panel::<ProjectPanel>(),
3054 workspace.right_dock().read(cx).panel::<ProjectPanel>(),
3055 workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
3056 ]
3057 .into_iter()
3058 .find_map(std::convert::identity)
3059 .expect("found no project panels")
3060 .read(cx);
3061 let (selected_worktree, selected_entry) = project_panel
3062 .selected_entry(cx)
3063 .expect("project panel should have a selected entry");
3064 assert_eq!(
3065 selected_worktree.abs_path().as_ref(),
3066 expected_worktree_path,
3067 "Unexpected project panel selected worktree path"
3068 );
3069 assert_eq!(
3070 selected_entry.path.as_ref(),
3071 expected_entry_path,
3072 "Unexpected project panel selected entry path"
3073 );
3074 }
3075
3076 // Open a file within an existing worktree.
3077 window
3078 .update(cx, |workspace, window, cx| {
3079 workspace.open_paths(
3080 vec![path!("/dir1/a.txt").into()],
3081 OpenOptions {
3082 visible: Some(OpenVisible::All),
3083 ..Default::default()
3084 },
3085 None,
3086 window,
3087 cx,
3088 )
3089 })
3090 .unwrap()
3091 .await;
3092 cx.run_until_parked();
3093 cx.read(|cx| {
3094 let workspace = workspace.read(cx);
3095 assert_project_panel_selection(
3096 workspace,
3097 Path::new(path!("/dir1")),
3098 rel_path("a.txt"),
3099 cx,
3100 );
3101 assert_eq!(
3102 workspace
3103 .active_pane()
3104 .read(cx)
3105 .active_item()
3106 .unwrap()
3107 .act_as::<Editor>(cx)
3108 .unwrap()
3109 .read(cx)
3110 .title(cx),
3111 "a.txt"
3112 );
3113 });
3114
3115 // Open a file outside of any existing worktree.
3116 window
3117 .update(cx, |workspace, window, cx| {
3118 workspace.open_paths(
3119 vec![path!("/dir2/b.txt").into()],
3120 OpenOptions {
3121 visible: Some(OpenVisible::All),
3122 ..Default::default()
3123 },
3124 None,
3125 window,
3126 cx,
3127 )
3128 })
3129 .unwrap()
3130 .await;
3131 cx.run_until_parked();
3132 cx.read(|cx| {
3133 let workspace = workspace.read(cx);
3134 assert_project_panel_selection(
3135 workspace,
3136 Path::new(path!("/dir2/b.txt")),
3137 rel_path(""),
3138 cx,
3139 );
3140 let worktree_roots = workspace
3141 .worktrees(cx)
3142 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3143 .collect::<HashSet<_>>();
3144 assert_eq!(
3145 worktree_roots,
3146 vec![path!("/dir1"), path!("/dir2/b.txt")]
3147 .into_iter()
3148 .map(Path::new)
3149 .collect(),
3150 );
3151 assert_eq!(
3152 workspace
3153 .active_pane()
3154 .read(cx)
3155 .active_item()
3156 .unwrap()
3157 .act_as::<Editor>(cx)
3158 .unwrap()
3159 .read(cx)
3160 .title(cx),
3161 "b.txt"
3162 );
3163 });
3164
3165 // Ensure opening a directory and one of its children only adds one worktree.
3166 window
3167 .update(cx, |workspace, window, cx| {
3168 workspace.open_paths(
3169 vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
3170 OpenOptions {
3171 visible: Some(OpenVisible::All),
3172 ..Default::default()
3173 },
3174 None,
3175 window,
3176 cx,
3177 )
3178 })
3179 .unwrap()
3180 .await;
3181 cx.run_until_parked();
3182 cx.read(|cx| {
3183 let workspace = workspace.read(cx);
3184 assert_project_panel_selection(
3185 workspace,
3186 Path::new(path!("/dir3")),
3187 rel_path("c.txt"),
3188 cx,
3189 );
3190 let worktree_roots = workspace
3191 .worktrees(cx)
3192 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3193 .collect::<HashSet<_>>();
3194 assert_eq!(
3195 worktree_roots,
3196 vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3197 .into_iter()
3198 .map(Path::new)
3199 .collect(),
3200 );
3201 assert_eq!(
3202 workspace
3203 .active_pane()
3204 .read(cx)
3205 .active_item()
3206 .unwrap()
3207 .act_as::<Editor>(cx)
3208 .unwrap()
3209 .read(cx)
3210 .title(cx),
3211 "c.txt"
3212 );
3213 });
3214
3215 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
3216 window
3217 .update(cx, |workspace, window, cx| {
3218 workspace.open_paths(
3219 vec![path!("/d.txt").into()],
3220 OpenOptions {
3221 visible: Some(OpenVisible::None),
3222 ..Default::default()
3223 },
3224 None,
3225 window,
3226 cx,
3227 )
3228 })
3229 .unwrap()
3230 .await;
3231 cx.run_until_parked();
3232 cx.read(|cx| {
3233 let workspace = workspace.read(cx);
3234 assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
3235 let worktree_roots = workspace
3236 .worktrees(cx)
3237 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3238 .collect::<HashSet<_>>();
3239 assert_eq!(
3240 worktree_roots,
3241 vec![
3242 path!("/dir1"),
3243 path!("/dir2/b.txt"),
3244 path!("/dir3"),
3245 path!("/d.txt")
3246 ]
3247 .into_iter()
3248 .map(Path::new)
3249 .collect(),
3250 );
3251
3252 let visible_worktree_roots = workspace
3253 .visible_worktrees(cx)
3254 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
3255 .collect::<HashSet<_>>();
3256 assert_eq!(
3257 visible_worktree_roots,
3258 vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
3259 .into_iter()
3260 .map(Path::new)
3261 .collect(),
3262 );
3263
3264 assert_eq!(
3265 workspace
3266 .active_pane()
3267 .read(cx)
3268 .active_item()
3269 .unwrap()
3270 .act_as::<Editor>(cx)
3271 .unwrap()
3272 .read(cx)
3273 .title(cx),
3274 "d.txt"
3275 );
3276 });
3277 }
3278
3279 #[gpui::test]
3280 async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
3281 let app_state = init_test(cx);
3282 cx.update(|cx| {
3283 cx.update_global::<SettingsStore, _>(|store, cx| {
3284 store.update_user_settings(cx, |project_settings| {
3285 project_settings.project.worktree.file_scan_exclusions =
3286 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
3287 });
3288 });
3289 });
3290 app_state
3291 .fs
3292 .as_fake()
3293 .insert_tree(
3294 path!("/root"),
3295 json!({
3296 ".gitignore": "ignored_dir\n",
3297 ".git": {
3298 "HEAD": "ref: refs/heads/main",
3299 },
3300 "regular_dir": {
3301 "file": "regular file contents",
3302 },
3303 "ignored_dir": {
3304 "ignored_subdir": {
3305 "file": "ignored subfile contents",
3306 },
3307 "file": "ignored file contents",
3308 },
3309 "excluded_dir": {
3310 "file": "excluded file contents",
3311 "ignored_subdir": {
3312 "file": "ignored subfile contents",
3313 },
3314 },
3315 }),
3316 )
3317 .await;
3318
3319 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3320 project.update(cx, |project, _cx| {
3321 project.languages().add(markdown_language())
3322 });
3323 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3324 let workspace = window.root(cx).unwrap();
3325
3326 let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
3327 let paths_to_open = [
3328 PathBuf::from(path!("/root/excluded_dir/file")),
3329 PathBuf::from(path!("/root/.git/HEAD")),
3330 PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
3331 ];
3332 let (opened_workspace, new_items) = cx
3333 .update(|cx| {
3334 workspace::open_paths(
3335 &paths_to_open,
3336 app_state,
3337 workspace::OpenOptions::default(),
3338 cx,
3339 )
3340 })
3341 .await
3342 .unwrap();
3343
3344 assert_eq!(
3345 opened_workspace.root(cx).unwrap().entity_id(),
3346 workspace.entity_id(),
3347 "Excluded files in subfolders of a workspace root should be opened in the workspace"
3348 );
3349 let mut opened_paths = cx.read(|cx| {
3350 assert_eq!(
3351 new_items.len(),
3352 paths_to_open.len(),
3353 "Expect to get the same number of opened items as submitted paths to open"
3354 );
3355 new_items
3356 .iter()
3357 .zip(paths_to_open.iter())
3358 .map(|(i, path)| {
3359 match i {
3360 Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
3361 Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
3362 None => None,
3363 }
3364 .flatten()
3365 })
3366 .collect::<Vec<_>>()
3367 });
3368 opened_paths.sort();
3369 assert_eq!(
3370 opened_paths,
3371 vec![
3372 None,
3373 Some(rel_path(".git/HEAD").into()),
3374 Some(rel_path("excluded_dir/file").into()),
3375 ],
3376 "Excluded files should get opened, excluded dir should not get opened"
3377 );
3378
3379 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3380 assert_eq!(
3381 initial_entries, entries,
3382 "Workspace entries should not change after opening excluded files and directories paths"
3383 );
3384
3385 cx.read(|cx| {
3386 let pane = workspace.read(cx).active_pane().read(cx);
3387 let mut opened_buffer_paths = pane
3388 .items()
3389 .map(|i| {
3390 i.project_path(cx)
3391 .expect("all excluded files that got open should have a path")
3392 .path
3393 })
3394 .collect::<Vec<_>>();
3395 opened_buffer_paths.sort();
3396 assert_eq!(
3397 opened_buffer_paths,
3398 vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
3399 "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
3400 );
3401 });
3402 }
3403
3404 #[gpui::test]
3405 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
3406 let app_state = init_test(cx);
3407 app_state
3408 .fs
3409 .as_fake()
3410 .insert_tree(path!("/root"), json!({ "a.txt": "" }))
3411 .await;
3412
3413 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3414 project.update(cx, |project, _cx| {
3415 project.languages().add(markdown_language())
3416 });
3417 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3418 let workspace = window.root(cx).unwrap();
3419
3420 // Open a file within an existing worktree.
3421 window
3422 .update(cx, |workspace, window, cx| {
3423 workspace.open_paths(
3424 vec![PathBuf::from(path!("/root/a.txt"))],
3425 OpenOptions {
3426 visible: Some(OpenVisible::All),
3427 ..Default::default()
3428 },
3429 None,
3430 window,
3431 cx,
3432 )
3433 })
3434 .unwrap()
3435 .await;
3436 let editor = cx.read(|cx| {
3437 let pane = workspace.read(cx).active_pane().read(cx);
3438 let item = pane.active_item().unwrap();
3439 item.downcast::<Editor>().unwrap()
3440 });
3441
3442 window
3443 .update(cx, |_, window, cx| {
3444 editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
3445 })
3446 .unwrap();
3447
3448 app_state
3449 .fs
3450 .as_fake()
3451 .insert_file(path!("/root/a.txt"), b"changed".to_vec())
3452 .await;
3453
3454 cx.run_until_parked();
3455 cx.read(|cx| assert!(editor.is_dirty(cx)));
3456 cx.read(|cx| assert!(editor.has_conflict(cx)));
3457
3458 let save_task = window
3459 .update(cx, |workspace, window, cx| {
3460 workspace.save_active_item(SaveIntent::Save, window, cx)
3461 })
3462 .unwrap();
3463 cx.background_executor.run_until_parked();
3464 cx.simulate_prompt_answer("Overwrite");
3465 save_task.await.unwrap();
3466 window
3467 .update(cx, |_, _, cx| {
3468 editor.update(cx, |editor, cx| {
3469 assert!(!editor.is_dirty(cx));
3470 assert!(!editor.has_conflict(cx));
3471 });
3472 })
3473 .unwrap();
3474 }
3475
3476 #[gpui::test]
3477 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
3478 let app_state = init_test(cx);
3479 app_state
3480 .fs
3481 .create_dir(Path::new(path!("/root")))
3482 .await
3483 .unwrap();
3484
3485 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3486 project.update(cx, |project, _| {
3487 project.languages().add(markdown_language());
3488 project.languages().add(rust_lang());
3489 });
3490 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3491 let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
3492
3493 // Create a new untitled buffer
3494 cx.dispatch_action(window.into(), NewFile);
3495 let editor = window
3496 .read_with(cx, |workspace, cx| {
3497 workspace
3498 .active_item(cx)
3499 .unwrap()
3500 .downcast::<Editor>()
3501 .unwrap()
3502 })
3503 .unwrap();
3504
3505 window
3506 .update(cx, |_, window, cx| {
3507 editor.update(cx, |editor, cx| {
3508 assert!(!editor.is_dirty(cx));
3509 assert_eq!(editor.title(cx), "untitled");
3510 assert!(Arc::ptr_eq(
3511 &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3512 &languages::PLAIN_TEXT
3513 ));
3514 editor.handle_input("hi", window, cx);
3515 assert!(editor.is_dirty(cx));
3516 });
3517 })
3518 .unwrap();
3519
3520 // Save the buffer. This prompts for a filename.
3521 let save_task = window
3522 .update(cx, |workspace, window, cx| {
3523 workspace.save_active_item(SaveIntent::Save, window, cx)
3524 })
3525 .unwrap();
3526 cx.background_executor.run_until_parked();
3527 cx.simulate_new_path_selection(|parent_dir| {
3528 assert_eq!(parent_dir, Path::new(path!("/root")));
3529 Some(parent_dir.join("the-new-name.rs"))
3530 });
3531 cx.read(|cx| {
3532 assert!(editor.is_dirty(cx));
3533 assert_eq!(editor.read(cx).title(cx), "hi");
3534 });
3535
3536 // When the save completes, the buffer's title is updated and the language is assigned based
3537 // on the path.
3538 save_task.await.unwrap();
3539 window
3540 .update(cx, |_, _, cx| {
3541 editor.update(cx, |editor, cx| {
3542 assert!(!editor.is_dirty(cx));
3543 assert_eq!(editor.title(cx), "the-new-name.rs");
3544 assert_eq!(
3545 editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3546 "Rust".into()
3547 );
3548 });
3549 })
3550 .unwrap();
3551
3552 // Edit the file and save it again. This time, there is no filename prompt.
3553 window
3554 .update(cx, |_, window, cx| {
3555 editor.update(cx, |editor, cx| {
3556 editor.handle_input(" there", window, cx);
3557 assert!(editor.is_dirty(cx));
3558 });
3559 })
3560 .unwrap();
3561
3562 let save_task = window
3563 .update(cx, |workspace, window, cx| {
3564 workspace.save_active_item(SaveIntent::Save, window, cx)
3565 })
3566 .unwrap();
3567 save_task.await.unwrap();
3568
3569 assert!(!cx.did_prompt_for_new_path());
3570 window
3571 .update(cx, |_, _, cx| {
3572 editor.update(cx, |editor, cx| {
3573 assert!(!editor.is_dirty(cx));
3574 assert_eq!(editor.title(cx), "the-new-name.rs")
3575 });
3576 })
3577 .unwrap();
3578
3579 // Open the same newly-created file in another pane item. The new editor should reuse
3580 // the same buffer.
3581 cx.dispatch_action(window.into(), NewFile);
3582 window
3583 .update(cx, |workspace, window, cx| {
3584 workspace.split_and_clone(
3585 workspace.active_pane().clone(),
3586 SplitDirection::Right,
3587 window,
3588 cx,
3589 )
3590 })
3591 .unwrap()
3592 .await
3593 .unwrap();
3594 window
3595 .update(cx, |workspace, window, cx| {
3596 workspace.open_path(
3597 (worktree.read(cx).id(), rel_path("the-new-name.rs")),
3598 None,
3599 true,
3600 window,
3601 cx,
3602 )
3603 })
3604 .unwrap()
3605 .await
3606 .unwrap();
3607 let editor2 = window
3608 .update(cx, |workspace, _, cx| {
3609 workspace
3610 .active_item(cx)
3611 .unwrap()
3612 .downcast::<Editor>()
3613 .unwrap()
3614 })
3615 .unwrap();
3616 cx.read(|cx| {
3617 assert_eq!(
3618 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3619 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3620 );
3621 })
3622 }
3623
3624 #[gpui::test]
3625 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3626 let app_state = init_test(cx);
3627 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3628
3629 let project = Project::test(app_state.fs.clone(), [], cx).await;
3630 project.update(cx, |project, _| {
3631 project.languages().add(rust_lang());
3632 project.languages().add(markdown_language());
3633 });
3634 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3635
3636 // Create a new untitled buffer
3637 cx.dispatch_action(window.into(), NewFile);
3638 let editor = window
3639 .read_with(cx, |workspace, cx| {
3640 workspace
3641 .active_item(cx)
3642 .unwrap()
3643 .downcast::<Editor>()
3644 .unwrap()
3645 })
3646 .unwrap();
3647 window
3648 .update(cx, |_, window, cx| {
3649 editor.update(cx, |editor, cx| {
3650 assert!(Arc::ptr_eq(
3651 &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3652 &languages::PLAIN_TEXT
3653 ));
3654 editor.handle_input("hi", window, cx);
3655 assert!(editor.is_dirty(cx));
3656 });
3657 })
3658 .unwrap();
3659
3660 // Save the buffer. This prompts for a filename.
3661 let save_task = window
3662 .update(cx, |workspace, window, cx| {
3663 workspace.save_active_item(SaveIntent::Save, window, cx)
3664 })
3665 .unwrap();
3666 cx.background_executor.run_until_parked();
3667 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3668 save_task.await.unwrap();
3669 // The buffer is not dirty anymore and the language is assigned based on the path.
3670 window
3671 .update(cx, |_, _, cx| {
3672 editor.update(cx, |editor, cx| {
3673 assert!(!editor.is_dirty(cx));
3674 assert_eq!(
3675 editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3676 "Rust".into()
3677 )
3678 });
3679 })
3680 .unwrap();
3681 }
3682
3683 #[gpui::test]
3684 async fn test_pane_actions(cx: &mut TestAppContext) {
3685 let app_state = init_test(cx);
3686 app_state
3687 .fs
3688 .as_fake()
3689 .insert_tree(
3690 path!("/root"),
3691 json!({
3692 "a": {
3693 "file1": "contents 1",
3694 "file2": "contents 2",
3695 "file3": "contents 3",
3696 },
3697 }),
3698 )
3699 .await;
3700
3701 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3702 project.update(cx, |project, _cx| {
3703 project.languages().add(markdown_language())
3704 });
3705 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3706 let workspace = window.root(cx).unwrap();
3707
3708 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3709 let file1 = entries[0].clone();
3710
3711 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3712
3713 window
3714 .update(cx, |w, window, cx| {
3715 w.open_path(file1.clone(), None, true, window, cx)
3716 })
3717 .unwrap()
3718 .await
3719 .unwrap();
3720
3721 let (editor_1, buffer) = window
3722 .update(cx, |_, window, cx| {
3723 pane_1.update(cx, |pane_1, cx| {
3724 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3725 assert_eq!(editor.project_path(cx), Some(file1.clone()));
3726 let buffer = editor.update(cx, |editor, cx| {
3727 editor.insert("dirt", window, cx);
3728 editor.buffer().downgrade()
3729 });
3730 (editor.downgrade(), buffer)
3731 })
3732 })
3733 .unwrap();
3734
3735 cx.dispatch_action(window.into(), pane::SplitRight);
3736 let editor_2 = cx.update(|cx| {
3737 let pane_2 = workspace.read(cx).active_pane().clone();
3738 assert_ne!(pane_1, pane_2);
3739
3740 let pane2_item = pane_2.read(cx).active_item().unwrap();
3741 assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3742
3743 pane2_item.downcast::<Editor>().unwrap().downgrade()
3744 });
3745 cx.dispatch_action(
3746 window.into(),
3747 workspace::CloseActiveItem {
3748 save_intent: None,
3749 close_pinned: false,
3750 },
3751 );
3752
3753 cx.background_executor.run_until_parked();
3754 window
3755 .read_with(cx, |workspace, _| {
3756 assert_eq!(workspace.panes().len(), 1);
3757 assert_eq!(workspace.active_pane(), &pane_1);
3758 })
3759 .unwrap();
3760
3761 cx.dispatch_action(
3762 window.into(),
3763 workspace::CloseActiveItem {
3764 save_intent: None,
3765 close_pinned: false,
3766 },
3767 );
3768 cx.background_executor.run_until_parked();
3769 cx.simulate_prompt_answer("Don't Save");
3770 cx.background_executor.run_until_parked();
3771
3772 window
3773 .update(cx, |workspace, _, cx| {
3774 assert_eq!(workspace.panes().len(), 1);
3775 assert!(workspace.active_item(cx).is_none());
3776 })
3777 .unwrap();
3778
3779 cx.background_executor
3780 .advance_clock(SERIALIZATION_THROTTLE_TIME);
3781 cx.update(|_| {});
3782 editor_1.assert_released();
3783 editor_2.assert_released();
3784 buffer.assert_released();
3785 }
3786
3787 #[gpui::test]
3788 async fn test_navigation(cx: &mut TestAppContext) {
3789 let app_state = init_test(cx);
3790 app_state
3791 .fs
3792 .as_fake()
3793 .insert_tree(
3794 path!("/root"),
3795 json!({
3796 "a": {
3797 "file1": "contents 1\n".repeat(20),
3798 "file2": "contents 2\n".repeat(20),
3799 "file3": "contents 3\n".repeat(20),
3800 },
3801 }),
3802 )
3803 .await;
3804
3805 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3806 project.update(cx, |project, _cx| {
3807 project.languages().add(markdown_language())
3808 });
3809 let workspace =
3810 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3811 let pane = workspace
3812 .read_with(cx, |workspace, _| workspace.active_pane().clone())
3813 .unwrap();
3814
3815 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3816 let file1 = entries[0].clone();
3817 let file2 = entries[1].clone();
3818 let file3 = entries[2].clone();
3819
3820 let editor1 = workspace
3821 .update(cx, |w, window, cx| {
3822 w.open_path(file1.clone(), None, true, window, cx)
3823 })
3824 .unwrap()
3825 .await
3826 .unwrap()
3827 .downcast::<Editor>()
3828 .unwrap();
3829 workspace
3830 .update(cx, |_, window, cx| {
3831 editor1.update(cx, |editor, cx| {
3832 editor.change_selections(Default::default(), window, cx, |s| {
3833 s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3834 ..DisplayPoint::new(DisplayRow(10), 0)])
3835 });
3836 });
3837 })
3838 .unwrap();
3839
3840 let editor2 = workspace
3841 .update(cx, |w, window, cx| {
3842 w.open_path(file2.clone(), None, true, window, cx)
3843 })
3844 .unwrap()
3845 .await
3846 .unwrap()
3847 .downcast::<Editor>()
3848 .unwrap();
3849 let editor3 = workspace
3850 .update(cx, |w, window, cx| {
3851 w.open_path(file3.clone(), None, true, window, cx)
3852 })
3853 .unwrap()
3854 .await
3855 .unwrap()
3856 .downcast::<Editor>()
3857 .unwrap();
3858
3859 workspace
3860 .update(cx, |_, window, cx| {
3861 editor3.update(cx, |editor, cx| {
3862 editor.change_selections(Default::default(), window, cx, |s| {
3863 s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3864 ..DisplayPoint::new(DisplayRow(12), 0)])
3865 });
3866 editor.newline(&Default::default(), window, cx);
3867 editor.newline(&Default::default(), window, cx);
3868 editor.move_down(&Default::default(), window, cx);
3869 editor.move_down(&Default::default(), window, cx);
3870 editor.save(
3871 SaveOptions {
3872 format: true,
3873 autosave: false,
3874 },
3875 project.clone(),
3876 window,
3877 cx,
3878 )
3879 })
3880 })
3881 .unwrap()
3882 .await
3883 .unwrap();
3884 workspace
3885 .update(cx, |_, window, cx| {
3886 editor3.update(cx, |editor, cx| {
3887 editor.set_scroll_position(point(0., 12.5), window, cx)
3888 });
3889 })
3890 .unwrap();
3891 assert_eq!(
3892 active_location(&workspace, cx),
3893 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3894 );
3895
3896 workspace
3897 .update(cx, |w, window, cx| {
3898 w.go_back(w.active_pane().downgrade(), window, cx)
3899 })
3900 .unwrap()
3901 .await
3902 .unwrap();
3903 assert_eq!(
3904 active_location(&workspace, cx),
3905 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3906 );
3907
3908 workspace
3909 .update(cx, |w, window, cx| {
3910 w.go_back(w.active_pane().downgrade(), window, cx)
3911 })
3912 .unwrap()
3913 .await
3914 .unwrap();
3915 assert_eq!(
3916 active_location(&workspace, cx),
3917 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3918 );
3919
3920 workspace
3921 .update(cx, |w, window, cx| {
3922 w.go_back(w.active_pane().downgrade(), window, cx)
3923 })
3924 .unwrap()
3925 .await
3926 .unwrap();
3927 assert_eq!(
3928 active_location(&workspace, cx),
3929 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3930 );
3931
3932 workspace
3933 .update(cx, |w, window, cx| {
3934 w.go_back(w.active_pane().downgrade(), window, cx)
3935 })
3936 .unwrap()
3937 .await
3938 .unwrap();
3939 assert_eq!(
3940 active_location(&workspace, cx),
3941 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3942 );
3943
3944 // Go back one more time and ensure we don't navigate past the first item in the history.
3945 workspace
3946 .update(cx, |w, window, cx| {
3947 w.go_back(w.active_pane().downgrade(), window, cx)
3948 })
3949 .unwrap()
3950 .await
3951 .unwrap();
3952 assert_eq!(
3953 active_location(&workspace, cx),
3954 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3955 );
3956
3957 workspace
3958 .update(cx, |w, window, cx| {
3959 w.go_forward(w.active_pane().downgrade(), window, cx)
3960 })
3961 .unwrap()
3962 .await
3963 .unwrap();
3964 assert_eq!(
3965 active_location(&workspace, cx),
3966 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3967 );
3968
3969 workspace
3970 .update(cx, |w, window, cx| {
3971 w.go_forward(w.active_pane().downgrade(), window, cx)
3972 })
3973 .unwrap()
3974 .await
3975 .unwrap();
3976 assert_eq!(
3977 active_location(&workspace, cx),
3978 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3979 );
3980
3981 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
3982 // location.
3983 workspace
3984 .update(cx, |_, window, cx| {
3985 pane.update(cx, |pane, cx| {
3986 let editor3_id = editor3.entity_id();
3987 drop(editor3);
3988 pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
3989 })
3990 })
3991 .unwrap()
3992 .await
3993 .unwrap();
3994 workspace
3995 .update(cx, |w, window, cx| {
3996 w.go_forward(w.active_pane().downgrade(), window, cx)
3997 })
3998 .unwrap()
3999 .await
4000 .unwrap();
4001 assert_eq!(
4002 active_location(&workspace, cx),
4003 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4004 );
4005
4006 workspace
4007 .update(cx, |w, window, cx| {
4008 w.go_forward(w.active_pane().downgrade(), window, cx)
4009 })
4010 .unwrap()
4011 .await
4012 .unwrap();
4013 assert_eq!(
4014 active_location(&workspace, cx),
4015 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
4016 );
4017
4018 workspace
4019 .update(cx, |w, window, cx| {
4020 w.go_back(w.active_pane().downgrade(), window, cx)
4021 })
4022 .unwrap()
4023 .await
4024 .unwrap();
4025 assert_eq!(
4026 active_location(&workspace, cx),
4027 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4028 );
4029
4030 // Go back to an item that has been closed and removed from disk
4031 workspace
4032 .update(cx, |_, window, cx| {
4033 pane.update(cx, |pane, cx| {
4034 let editor2_id = editor2.entity_id();
4035 drop(editor2);
4036 pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
4037 })
4038 })
4039 .unwrap()
4040 .await
4041 .unwrap();
4042 app_state
4043 .fs
4044 .remove_file(Path::new(path!("/root/a/file2")), Default::default())
4045 .await
4046 .unwrap();
4047 cx.background_executor.run_until_parked();
4048
4049 workspace
4050 .update(cx, |w, window, cx| {
4051 w.go_back(w.active_pane().downgrade(), window, cx)
4052 })
4053 .unwrap()
4054 .await
4055 .unwrap();
4056 assert_eq!(
4057 active_location(&workspace, cx),
4058 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4059 );
4060 workspace
4061 .update(cx, |w, window, cx| {
4062 w.go_forward(w.active_pane().downgrade(), window, cx)
4063 })
4064 .unwrap()
4065 .await
4066 .unwrap();
4067 assert_eq!(
4068 active_location(&workspace, cx),
4069 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
4070 );
4071
4072 // Modify file to collapse multiple nav history entries into the same location.
4073 // Ensure we don't visit the same location twice when navigating.
4074 workspace
4075 .update(cx, |_, window, cx| {
4076 editor1.update(cx, |editor, cx| {
4077 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4078 s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
4079 ..DisplayPoint::new(DisplayRow(15), 0)])
4080 })
4081 });
4082 })
4083 .unwrap();
4084 for _ in 0..5 {
4085 workspace
4086 .update(cx, |_, window, cx| {
4087 editor1.update(cx, |editor, cx| {
4088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4089 s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
4090 ..DisplayPoint::new(DisplayRow(3), 0)])
4091 });
4092 });
4093 })
4094 .unwrap();
4095
4096 workspace
4097 .update(cx, |_, window, cx| {
4098 editor1.update(cx, |editor, cx| {
4099 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4100 s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
4101 ..DisplayPoint::new(DisplayRow(13), 0)])
4102 })
4103 });
4104 })
4105 .unwrap();
4106 }
4107 workspace
4108 .update(cx, |_, window, cx| {
4109 editor1.update(cx, |editor, cx| {
4110 editor.transact(window, cx, |editor, window, cx| {
4111 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4112 s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
4113 ..DisplayPoint::new(DisplayRow(14), 0)])
4114 });
4115 editor.insert("", window, cx);
4116 })
4117 });
4118 })
4119 .unwrap();
4120
4121 workspace
4122 .update(cx, |_, window, cx| {
4123 editor1.update(cx, |editor, cx| {
4124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4125 s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
4126 ..DisplayPoint::new(DisplayRow(1), 0)])
4127 })
4128 });
4129 })
4130 .unwrap();
4131 workspace
4132 .update(cx, |w, window, cx| {
4133 w.go_back(w.active_pane().downgrade(), window, cx)
4134 })
4135 .unwrap()
4136 .await
4137 .unwrap();
4138 assert_eq!(
4139 active_location(&workspace, cx),
4140 (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
4141 );
4142 workspace
4143 .update(cx, |w, window, cx| {
4144 w.go_back(w.active_pane().downgrade(), window, cx)
4145 })
4146 .unwrap()
4147 .await
4148 .unwrap();
4149 assert_eq!(
4150 active_location(&workspace, cx),
4151 (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
4152 );
4153
4154 fn active_location(
4155 workspace: &WindowHandle<Workspace>,
4156 cx: &mut TestAppContext,
4157 ) -> (ProjectPath, DisplayPoint, f64) {
4158 workspace
4159 .update(cx, |workspace, _, cx| {
4160 let item = workspace.active_item(cx).unwrap();
4161 let editor = item.downcast::<Editor>().unwrap();
4162 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
4163 (
4164 editor
4165 .selections
4166 .display_ranges(&editor.display_snapshot(cx)),
4167 editor.scroll_position(cx),
4168 )
4169 });
4170 (
4171 item.project_path(cx).unwrap(),
4172 selections[0].start,
4173 scroll_position.y,
4174 )
4175 })
4176 .unwrap()
4177 }
4178 }
4179
4180 #[gpui::test]
4181 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
4182 let app_state = init_test(cx);
4183 app_state
4184 .fs
4185 .as_fake()
4186 .insert_tree(
4187 path!("/root"),
4188 json!({
4189 "a": {
4190 "file1": "",
4191 "file2": "",
4192 "file3": "",
4193 "file4": "",
4194 },
4195 }),
4196 )
4197 .await;
4198
4199 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
4200 project.update(cx, |project, _cx| {
4201 project.languages().add(markdown_language())
4202 });
4203 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4204 let pane = workspace
4205 .read_with(cx, |workspace, _| workspace.active_pane().clone())
4206 .unwrap();
4207
4208 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
4209 let file1 = entries[0].clone();
4210 let file2 = entries[1].clone();
4211 let file3 = entries[2].clone();
4212 let file4 = entries[3].clone();
4213
4214 let file1_item_id = workspace
4215 .update(cx, |w, window, cx| {
4216 w.open_path(file1.clone(), None, true, window, cx)
4217 })
4218 .unwrap()
4219 .await
4220 .unwrap()
4221 .item_id();
4222 let file2_item_id = workspace
4223 .update(cx, |w, window, cx| {
4224 w.open_path(file2.clone(), None, true, window, cx)
4225 })
4226 .unwrap()
4227 .await
4228 .unwrap()
4229 .item_id();
4230 let file3_item_id = workspace
4231 .update(cx, |w, window, cx| {
4232 w.open_path(file3.clone(), None, true, window, cx)
4233 })
4234 .unwrap()
4235 .await
4236 .unwrap()
4237 .item_id();
4238 let file4_item_id = workspace
4239 .update(cx, |w, window, cx| {
4240 w.open_path(file4.clone(), None, true, window, cx)
4241 })
4242 .unwrap()
4243 .await
4244 .unwrap()
4245 .item_id();
4246 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4247
4248 // Close all the pane items in some arbitrary order.
4249 workspace
4250 .update(cx, |_, window, cx| {
4251 pane.update(cx, |pane, cx| {
4252 pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
4253 })
4254 })
4255 .unwrap()
4256 .await
4257 .unwrap();
4258 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4259
4260 workspace
4261 .update(cx, |_, window, cx| {
4262 pane.update(cx, |pane, cx| {
4263 pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
4264 })
4265 })
4266 .unwrap()
4267 .await
4268 .unwrap();
4269 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4270
4271 workspace
4272 .update(cx, |_, window, cx| {
4273 pane.update(cx, |pane, cx| {
4274 pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
4275 })
4276 })
4277 .unwrap()
4278 .await
4279 .unwrap();
4280 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4281 workspace
4282 .update(cx, |_, window, cx| {
4283 pane.update(cx, |pane, cx| {
4284 pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
4285 })
4286 })
4287 .unwrap()
4288 .await
4289 .unwrap();
4290
4291 assert_eq!(active_path(&workspace, cx), None);
4292
4293 // Reopen all the closed items, ensuring they are reopened in the same order
4294 // in which they were closed.
4295 workspace
4296 .update(cx, Workspace::reopen_closed_item)
4297 .unwrap()
4298 .await
4299 .unwrap();
4300 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4301
4302 workspace
4303 .update(cx, Workspace::reopen_closed_item)
4304 .unwrap()
4305 .await
4306 .unwrap();
4307 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4308
4309 workspace
4310 .update(cx, Workspace::reopen_closed_item)
4311 .unwrap()
4312 .await
4313 .unwrap();
4314 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4315
4316 workspace
4317 .update(cx, Workspace::reopen_closed_item)
4318 .unwrap()
4319 .await
4320 .unwrap();
4321 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4322
4323 // Reopening past the last closed item is a no-op.
4324 workspace
4325 .update(cx, Workspace::reopen_closed_item)
4326 .unwrap()
4327 .await
4328 .unwrap();
4329 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4330
4331 // Reopening closed items doesn't interfere with navigation history.
4332 workspace
4333 .update(cx, |workspace, window, cx| {
4334 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4335 })
4336 .unwrap()
4337 .await
4338 .unwrap();
4339 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4340
4341 workspace
4342 .update(cx, |workspace, window, cx| {
4343 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4344 })
4345 .unwrap()
4346 .await
4347 .unwrap();
4348 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4349
4350 workspace
4351 .update(cx, |workspace, window, cx| {
4352 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4353 })
4354 .unwrap()
4355 .await
4356 .unwrap();
4357 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4358
4359 workspace
4360 .update(cx, |workspace, window, cx| {
4361 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4362 })
4363 .unwrap()
4364 .await
4365 .unwrap();
4366 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4367
4368 workspace
4369 .update(cx, |workspace, window, cx| {
4370 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4371 })
4372 .unwrap()
4373 .await
4374 .unwrap();
4375 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4376
4377 workspace
4378 .update(cx, |workspace, window, cx| {
4379 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4380 })
4381 .unwrap()
4382 .await
4383 .unwrap();
4384 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4385
4386 workspace
4387 .update(cx, |workspace, window, cx| {
4388 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4389 })
4390 .unwrap()
4391 .await
4392 .unwrap();
4393 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4394
4395 workspace
4396 .update(cx, |workspace, window, cx| {
4397 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4398 })
4399 .unwrap()
4400 .await
4401 .unwrap();
4402 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4403
4404 fn active_path(
4405 workspace: &WindowHandle<Workspace>,
4406 cx: &TestAppContext,
4407 ) -> Option<ProjectPath> {
4408 workspace
4409 .read_with(cx, |workspace, cx| {
4410 let item = workspace.active_item(cx)?;
4411 item.project_path(cx)
4412 })
4413 .unwrap()
4414 }
4415 }
4416
4417 fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
4418 cx.update(|cx| {
4419 let app_state = AppState::test(cx);
4420
4421 theme::init(theme::LoadThemes::JustBase, cx);
4422 client::init(&app_state.client, cx);
4423 workspace::init(app_state.clone(), cx);
4424 onboarding::init(cx);
4425 app_state
4426 })
4427 }
4428
4429 actions!(test_only, [ActionA, ActionB]);
4430
4431 #[gpui::test]
4432 async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
4433 let executor = cx.executor();
4434 let app_state = init_keymap_test(cx);
4435 let project = Project::test(app_state.fs.clone(), [], cx).await;
4436 let workspace =
4437 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4438
4439 // From the Atom keymap
4440 use workspace::ActivatePreviousPane;
4441 // From the JetBrains keymap
4442 use workspace::ActivatePreviousItem;
4443
4444 app_state
4445 .fs
4446 .save(
4447 "/settings.json".as_ref(),
4448 &r#"{"base_keymap": "Atom"}"#.into(),
4449 Default::default(),
4450 )
4451 .await
4452 .unwrap();
4453
4454 app_state
4455 .fs
4456 .save(
4457 "/keymap.json".as_ref(),
4458 &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4459 Default::default(),
4460 )
4461 .await
4462 .unwrap();
4463 executor.run_until_parked();
4464 cx.update(|cx| {
4465 let settings_rx = watch_config_file(
4466 &executor,
4467 app_state.fs.clone(),
4468 PathBuf::from("/settings.json"),
4469 );
4470 let keymap_rx = watch_config_file(
4471 &executor,
4472 app_state.fs.clone(),
4473 PathBuf::from("/keymap.json"),
4474 );
4475 let global_settings_rx = watch_config_file(
4476 &executor,
4477 app_state.fs.clone(),
4478 PathBuf::from("/global_settings.json"),
4479 );
4480 handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4481 handle_keymap_file_changes(keymap_rx, cx);
4482 });
4483 workspace
4484 .update(cx, |workspace, _, cx| {
4485 workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4486 workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4487 workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
4488 workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
4489 cx.notify();
4490 })
4491 .unwrap();
4492 executor.run_until_parked();
4493 // Test loading the keymap base at all
4494 assert_key_bindings_for(
4495 workspace.into(),
4496 cx,
4497 vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4498 line!(),
4499 );
4500
4501 // Test modifying the users keymap, while retaining the base keymap
4502 app_state
4503 .fs
4504 .save(
4505 "/keymap.json".as_ref(),
4506 &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(),
4507 Default::default(),
4508 )
4509 .await
4510 .unwrap();
4511
4512 executor.run_until_parked();
4513
4514 assert_key_bindings_for(
4515 workspace.into(),
4516 cx,
4517 vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)],
4518 line!(),
4519 );
4520
4521 // Test modifying the base, while retaining the users keymap
4522 app_state
4523 .fs
4524 .save(
4525 "/settings.json".as_ref(),
4526 &r#"{"base_keymap": "JetBrains"}"#.into(),
4527 Default::default(),
4528 )
4529 .await
4530 .unwrap();
4531
4532 executor.run_until_parked();
4533
4534 assert_key_bindings_for(
4535 workspace.into(),
4536 cx,
4537 vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)],
4538 line!(),
4539 );
4540 }
4541
4542 #[gpui::test]
4543 async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4544 let executor = cx.executor();
4545 let app_state = init_keymap_test(cx);
4546 let project = Project::test(app_state.fs.clone(), [], cx).await;
4547 let workspace =
4548 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4549
4550 // From the Atom keymap
4551 use workspace::ActivatePreviousPane;
4552 // From the JetBrains keymap
4553 use diagnostics::Deploy;
4554
4555 workspace
4556 .update(cx, |workspace, _, _| {
4557 workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4558 workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4559 workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4560 })
4561 .unwrap();
4562 app_state
4563 .fs
4564 .save(
4565 "/settings.json".as_ref(),
4566 &r#"{"base_keymap": "Atom"}"#.into(),
4567 Default::default(),
4568 )
4569 .await
4570 .unwrap();
4571 app_state
4572 .fs
4573 .save(
4574 "/keymap.json".as_ref(),
4575 &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4576 Default::default(),
4577 )
4578 .await
4579 .unwrap();
4580
4581 cx.update(|cx| {
4582 let settings_rx = watch_config_file(
4583 &executor,
4584 app_state.fs.clone(),
4585 PathBuf::from("/settings.json"),
4586 );
4587 let keymap_rx = watch_config_file(
4588 &executor,
4589 app_state.fs.clone(),
4590 PathBuf::from("/keymap.json"),
4591 );
4592
4593 let global_settings_rx = watch_config_file(
4594 &executor,
4595 app_state.fs.clone(),
4596 PathBuf::from("/global_settings.json"),
4597 );
4598 handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4599 handle_keymap_file_changes(keymap_rx, cx);
4600 });
4601
4602 cx.background_executor.run_until_parked();
4603
4604 cx.background_executor.run_until_parked();
4605 // Test loading the keymap base at all
4606 assert_key_bindings_for(
4607 workspace.into(),
4608 cx,
4609 vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4610 line!(),
4611 );
4612
4613 // Test disabling the key binding for the base keymap
4614 app_state
4615 .fs
4616 .save(
4617 "/keymap.json".as_ref(),
4618 &r#"[{"bindings": {"backspace": null}}]"#.into(),
4619 Default::default(),
4620 )
4621 .await
4622 .unwrap();
4623
4624 cx.background_executor.run_until_parked();
4625
4626 assert_key_bindings_for(
4627 workspace.into(),
4628 cx,
4629 vec![("k", &ActivatePreviousPane)],
4630 line!(),
4631 );
4632
4633 // Test modifying the base, while retaining the users keymap
4634 app_state
4635 .fs
4636 .save(
4637 "/settings.json".as_ref(),
4638 &r#"{"base_keymap": "JetBrains"}"#.into(),
4639 Default::default(),
4640 )
4641 .await
4642 .unwrap();
4643
4644 cx.background_executor.run_until_parked();
4645
4646 assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4647 }
4648
4649 #[gpui::test]
4650 async fn test_generate_keymap_json_schema_for_registered_actions(
4651 cx: &mut gpui::TestAppContext,
4652 ) {
4653 init_keymap_test(cx);
4654 cx.update(|cx| {
4655 // Make sure it doesn't panic.
4656 KeymapFile::generate_json_schema_for_registered_actions(cx);
4657 });
4658 }
4659
4660 /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
4661 /// and let you know when introducing a new namespace.
4662 #[gpui::test]
4663 async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
4664 use itertools::Itertools;
4665
4666 init_keymap_test(cx);
4667 cx.update(|cx| {
4668 let all_actions = cx.all_action_names();
4669
4670 let mut actions_without_namespace = Vec::new();
4671 let all_namespaces = all_actions
4672 .iter()
4673 .filter_map(|action_name| {
4674 let namespace = action_name
4675 .split("::")
4676 .collect::<Vec<_>>()
4677 .into_iter()
4678 .rev()
4679 .skip(1)
4680 .rev()
4681 .join("::");
4682 if namespace.is_empty() {
4683 actions_without_namespace.push(*action_name);
4684 }
4685 if &namespace == "test_only" || &namespace == "stories" {
4686 None
4687 } else {
4688 Some(namespace)
4689 }
4690 })
4691 .sorted()
4692 .dedup()
4693 .collect::<Vec<_>>();
4694 assert_eq!(actions_without_namespace, Vec::<&str>::new());
4695
4696 let expected_namespaces = vec![
4697 "action",
4698 "activity_indicator",
4699 "agent",
4700 #[cfg(not(target_os = "macos"))]
4701 "app_menu",
4702 "assistant",
4703 "assistant2",
4704 "auto_update",
4705 "branches",
4706 "buffer_search",
4707 "channel_modal",
4708 "cli",
4709 "client",
4710 "collab",
4711 "collab_panel",
4712 "command_palette",
4713 "console",
4714 "context_server",
4715 "copilot",
4716 "debug_panel",
4717 "debugger",
4718 "dev",
4719 "diagnostics",
4720 "edit_prediction",
4721 "editor",
4722 "feedback",
4723 "file_finder",
4724 "git",
4725 "git_onboarding",
4726 "git_panel",
4727 "go_to_line",
4728 "icon_theme_selector",
4729 "journal",
4730 "keymap_editor",
4731 "keystroke_input",
4732 "language_selector",
4733 "line_ending_selector",
4734 "lsp_tool",
4735 "markdown",
4736 "menu",
4737 "notebook",
4738 "notification_panel",
4739 "onboarding",
4740 "outline",
4741 "outline_panel",
4742 "pane",
4743 "panel",
4744 "picker",
4745 "project_panel",
4746 "project_search",
4747 "project_symbols",
4748 "projects",
4749 "repl",
4750 "rules_library",
4751 "search",
4752 "settings_editor",
4753 "settings_profile_selector",
4754 "snippets",
4755 "stash_picker",
4756 "supermaven",
4757 "svg",
4758 "syntax_tree_view",
4759 "tab_switcher",
4760 "task",
4761 "terminal",
4762 "terminal_panel",
4763 "theme_selector",
4764 "toast",
4765 "toolchain",
4766 "variable_list",
4767 "vim",
4768 "window",
4769 "workspace",
4770 "zed",
4771 "zed_actions",
4772 "zed_predict_onboarding",
4773 "zeta",
4774 ];
4775 assert_eq!(
4776 all_namespaces,
4777 expected_namespaces
4778 .into_iter()
4779 .map(|namespace| namespace.to_string())
4780 .sorted()
4781 .collect::<Vec<_>>()
4782 );
4783 });
4784 }
4785
4786 #[gpui::test]
4787 fn test_bundled_settings_and_themes(cx: &mut App) {
4788 cx.text_system()
4789 .add_fonts(vec![
4790 Assets
4791 .load("fonts/lilex/Lilex-Regular.ttf")
4792 .unwrap()
4793 .unwrap(),
4794 Assets
4795 .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
4796 .unwrap()
4797 .unwrap(),
4798 ])
4799 .unwrap();
4800 let themes = ThemeRegistry::default();
4801 settings::init(cx);
4802 theme::init(theme::LoadThemes::JustBase, cx);
4803
4804 let mut has_default_theme = false;
4805 for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4806 let theme = themes.get(&theme_name).unwrap();
4807 assert_eq!(theme.name, theme_name);
4808 if theme.name.as_ref() == "One Dark" {
4809 has_default_theme = true;
4810 }
4811 }
4812 assert!(has_default_theme);
4813 }
4814
4815 #[gpui::test]
4816 async fn test_bundled_files_editor(cx: &mut TestAppContext) {
4817 let app_state = init_test(cx);
4818 cx.update(init);
4819
4820 let project = Project::test(app_state.fs.clone(), [], cx).await;
4821 let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4822
4823 cx.update(|cx| {
4824 cx.dispatch_action(&OpenDefaultSettings);
4825 });
4826 cx.run_until_parked();
4827
4828 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
4829
4830 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
4831 let active_editor = workspace
4832 .update(cx, |workspace, _, cx| {
4833 workspace.active_item_as::<Editor>(cx)
4834 })
4835 .unwrap();
4836 assert!(
4837 active_editor.is_some(),
4838 "Settings action should have opened an editor with the default file contents"
4839 );
4840
4841 let active_editor = active_editor.unwrap();
4842 assert!(
4843 active_editor.read_with(cx, |editor, cx| editor.read_only(cx)),
4844 "Default settings should be readonly"
4845 );
4846 assert!(
4847 active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()),
4848 "The underlying buffer should also be readonly for the shipped default settings"
4849 );
4850 }
4851
4852 #[gpui::test]
4853 async fn test_bundled_languages(cx: &mut TestAppContext) {
4854 let fs = fs::FakeFs::new(cx.background_executor.clone());
4855 env_logger::builder().is_test(true).try_init().ok();
4856 let settings = cx.update(SettingsStore::test);
4857 cx.set_global(settings);
4858 let languages = LanguageRegistry::test(cx.executor());
4859 let languages = Arc::new(languages);
4860 let node_runtime = node_runtime::NodeRuntime::unavailable();
4861 cx.update(|cx| {
4862 languages::init(languages.clone(), fs, node_runtime, cx);
4863 });
4864 for name in languages.language_names() {
4865 languages
4866 .language_for_name(name.as_ref())
4867 .await
4868 .with_context(|| format!("language name {name}"))
4869 .unwrap();
4870 }
4871 cx.run_until_parked();
4872 }
4873
4874 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4875 init_test_with_state(cx, cx.update(AppState::test))
4876 }
4877
4878 fn init_test_with_state(
4879 cx: &mut TestAppContext,
4880 mut app_state: Arc<AppState>,
4881 ) -> Arc<AppState> {
4882 cx.update(move |cx| {
4883 env_logger::builder().is_test(true).try_init().ok();
4884
4885 let state = Arc::get_mut(&mut app_state).unwrap();
4886 state.build_window_options = build_window_options;
4887 app_state.languages.add(markdown_language());
4888
4889 gpui_tokio::init(cx);
4890 theme::init(theme::LoadThemes::JustBase, cx);
4891 audio::init(cx);
4892 channel::init(&app_state.client, app_state.user_store.clone(), cx);
4893 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4894 notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4895 workspace::init(app_state.clone(), cx);
4896 release_channel::init(SemanticVersion::default(), cx);
4897 command_palette::init(cx);
4898 editor::init(cx);
4899 collab_ui::init(&app_state, cx);
4900 git_ui::init(cx);
4901 project_panel::init(cx);
4902 outline_panel::init(cx);
4903 terminal_view::init(cx);
4904 copilot::copilot_chat::init(
4905 app_state.fs.clone(),
4906 app_state.client.http_client(),
4907 copilot::copilot_chat::CopilotChatConfiguration::default(),
4908 cx,
4909 );
4910 image_viewer::init(cx);
4911 language_model::init(app_state.client.clone(), cx);
4912 language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
4913 web_search::init(cx);
4914 web_search_providers::init(app_state.client.clone(), cx);
4915 let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
4916 agent_ui::init(
4917 app_state.fs.clone(),
4918 app_state.client.clone(),
4919 prompt_builder.clone(),
4920 app_state.languages.clone(),
4921 false,
4922 cx,
4923 );
4924 repl::init(app_state.fs.clone(), cx);
4925 repl::notebook::init(cx);
4926 tasks_ui::init(cx);
4927 project::debugger::breakpoint_store::BreakpointStore::init(
4928 &app_state.client.clone().into(),
4929 );
4930 project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
4931 debugger_ui::init(cx);
4932 initialize_workspace(app_state.clone(), prompt_builder, cx);
4933 search::init(cx);
4934 app_state
4935 })
4936 }
4937
4938 fn rust_lang() -> Arc<language::Language> {
4939 Arc::new(language::Language::new(
4940 language::LanguageConfig {
4941 name: "Rust".into(),
4942 matcher: LanguageMatcher {
4943 path_suffixes: vec!["rs".to_string()],
4944 ..Default::default()
4945 },
4946 ..Default::default()
4947 },
4948 Some(tree_sitter_rust::LANGUAGE.into()),
4949 ))
4950 }
4951
4952 fn markdown_language() -> Arc<language::Language> {
4953 Arc::new(language::Language::new(
4954 language::LanguageConfig {
4955 name: "Markdown".into(),
4956 matcher: LanguageMatcher {
4957 path_suffixes: vec!["md".to_string()],
4958 ..Default::default()
4959 },
4960 ..Default::default()
4961 },
4962 Some(tree_sitter_md::LANGUAGE.into()),
4963 ))
4964 }
4965
4966 #[track_caller]
4967 fn assert_key_bindings_for(
4968 window: AnyWindowHandle,
4969 cx: &TestAppContext,
4970 actions: Vec<(&'static str, &dyn Action)>,
4971 line: u32,
4972 ) {
4973 let available_actions = cx
4974 .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
4975 .unwrap();
4976 for (key, action) in actions {
4977 let bindings = cx
4978 .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
4979 .unwrap();
4980 // assert that...
4981 assert!(
4982 available_actions.iter().any(|bound_action| {
4983 // actions match...
4984 bound_action.partial_eq(action)
4985 }),
4986 "On {} Failed to find {}",
4987 line,
4988 action.name(),
4989 );
4990 assert!(
4991 // and key strokes contain the given key
4992 bindings
4993 .into_iter()
4994 .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
4995 "On {} Failed to find {} with key binding {}",
4996 line,
4997 action.name(),
4998 key
4999 );
5000 }
5001 }
5002
5003 #[gpui::test]
5004 async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
5005 // Use the proper initialization for runtime state
5006 let app_state = init_keymap_test(cx);
5007
5008 eprintln!("Running test_opening_project_settings_when_excluded");
5009
5010 // 1. Set up a project with some project settings
5011 let settings_init =
5012 r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
5013 app_state
5014 .fs
5015 .as_fake()
5016 .insert_tree(
5017 Path::new("/root"),
5018 json!({
5019 ".zed": {
5020 "settings.json": settings_init
5021 }
5022 }),
5023 )
5024 .await;
5025
5026 eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
5027
5028 // 2. Create a project with the file system and load it
5029 let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
5030
5031 // Save original settings content for comparison
5032 let original_settings = app_state
5033 .fs
5034 .load(Path::new("/root/.zed/settings.json"))
5035 .await
5036 .unwrap();
5037
5038 let original_settings_str = original_settings.clone();
5039
5040 // Verify settings exist on disk and have expected content
5041 eprintln!("Original settings content: {}", original_settings_str);
5042 assert!(
5043 original_settings_str.contains("UNIQUEVALUE"),
5044 "Test setup failed - settings file doesn't contain our marker"
5045 );
5046
5047 // 3. Add .zed to file scan exclusions in user settings
5048 cx.update_global::<SettingsStore, _>(|store, cx| {
5049 store.update_user_settings(cx, |worktree_settings| {
5050 worktree_settings.project.worktree.file_scan_exclusions =
5051 Some(vec![".zed".to_string()]);
5052 });
5053 });
5054
5055 eprintln!("Added .zed to file_scan_exclusions in settings");
5056
5057 // 4. Run tasks to apply settings
5058 cx.background_executor.run_until_parked();
5059
5060 // 5. Critical: Verify .zed is actually excluded from worktree
5061 let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
5062
5063 let has_zed_entry =
5064 cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
5065
5066 eprintln!(
5067 "Is .zed directory visible in worktree after exclusion: {}",
5068 has_zed_entry
5069 );
5070
5071 // This assertion verifies the test is set up correctly to show the bug
5072 // If .zed is not excluded, the test will fail here
5073 assert!(
5074 !has_zed_entry,
5075 "Test precondition failed: .zed directory should be excluded but was found in worktree"
5076 );
5077
5078 // 6. Create workspace and trigger the actual function that causes the bug
5079 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5080 window
5081 .update(cx, |workspace, window, cx| {
5082 // Call the exact function that contains the bug
5083 eprintln!("About to call open_project_settings_file");
5084 open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx);
5085 })
5086 .unwrap();
5087
5088 // 7. Run background tasks until completion
5089 cx.background_executor.run_until_parked();
5090
5091 // 8. Verify file contents after calling function
5092 let new_content = app_state
5093 .fs
5094 .load(Path::new("/root/.zed/settings.json"))
5095 .await
5096 .unwrap();
5097
5098 let new_content_str = new_content;
5099 eprintln!("New settings content: {}", new_content_str);
5100
5101 // The bug causes the settings to be overwritten with empty settings
5102 // So if the unique value is no longer present, the bug has been reproduced
5103 let bug_exists = !new_content_str.contains("UNIQUEVALUE");
5104 eprintln!("Bug reproduced: {}", bug_exists);
5105
5106 // This assertion should fail if the bug exists - showing the bug is real
5107 assert!(
5108 new_content_str.contains("UNIQUEVALUE"),
5109 "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
5110 );
5111 }
5112
5113 #[gpui::test]
5114 async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) {
5115 let app_state = init_test(cx);
5116 let paths = [PathBuf::from(path!("/dir/document.txt"))];
5117
5118 app_state
5119 .fs
5120 .as_fake()
5121 .insert_tree(
5122 path!("/dir"),
5123 json!({
5124 "document.txt": "Some of the documentation's content."
5125 }),
5126 )
5127 .await;
5128
5129 let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5130 let window_a =
5131 cx.add_window(|window, cx| Workspace::test_new(project_a.clone(), window, cx));
5132
5133 let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5134 let window_b =
5135 cx.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx));
5136
5137 let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await;
5138 let window_c =
5139 cx.add_window(|window, cx| Workspace::test_new(project_c.clone(), window, cx));
5140
5141 for window in [window_a, window_b, window_c] {
5142 let _ = cx.update_window(*window, |_, window, _| {
5143 window.activate_window();
5144 });
5145
5146 cx.update(|cx| {
5147 let open_options = OpenOptions {
5148 prefer_focused_window: true,
5149 ..Default::default()
5150 };
5151
5152 workspace::open_paths(&paths, app_state.clone(), open_options, cx)
5153 })
5154 .await
5155 .unwrap();
5156
5157 cx.update_window(*window, |_, window, _| assert!(window.is_window_active()))
5158 .unwrap();
5159
5160 let _ = window.read_with(cx, |workspace, cx| {
5161 let pane = workspace.active_pane().read(cx);
5162 let project_path = pane.active_item().unwrap().project_path(cx).unwrap();
5163
5164 assert_eq!(
5165 project_path.path.as_ref().as_std_path().to_str().unwrap(),
5166 path!("document.txt")
5167 )
5168 });
5169 }
5170 }
5171}