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