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