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