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