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