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