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