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