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