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