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