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