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