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.read_with(cx, |tree, _| {
1673 tree.abs_path().join(settings_relative_path.as_std_path())
1674 })?;
1675
1676 let fs = project.read_with(cx, |project, _| project.fs().clone())?;
1677
1678 fs.metadata(&full_path)
1679 .await
1680 .ok()
1681 .flatten()
1682 .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo)
1683 };
1684
1685 if !file_exists {
1686 if let Some(dir_path) = settings_relative_path.parent()
1687 && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())?
1688 {
1689 project
1690 .update(cx, |project, cx| {
1691 project.create_entry((tree_id, dir_path), true, cx)
1692 })?
1693 .await
1694 .context("worktree was removed")?;
1695 }
1696
1697 if worktree.read_with(cx, |tree, _| {
1698 tree.entry_for_path(settings_relative_path).is_none()
1699 })? {
1700 project
1701 .update(cx, |project, cx| {
1702 project.create_entry((tree_id, settings_relative_path), false, cx)
1703 })?
1704 .await
1705 .context("worktree was removed")?;
1706 }
1707 }
1708
1709 let editor = workspace
1710 .update_in(cx, |workspace, window, cx| {
1711 workspace.open_path((tree_id, settings_relative_path), None, true, window, cx)
1712 })?
1713 .await?
1714 .downcast::<Editor>()
1715 .context("unexpected item type: expected editor item")?;
1716
1717 editor
1718 .downgrade()
1719 .update(cx, |editor, cx| {
1720 if let Some(buffer) = editor.buffer().read(cx).as_singleton()
1721 && buffer.read(cx).is_empty()
1722 {
1723 buffer.update(cx, |buffer, cx| {
1724 buffer.edit([(0..0, initial_contents)], None, cx)
1725 });
1726 }
1727 })
1728 .ok();
1729
1730 anyhow::Ok(())
1731 })
1732 .detach();
1733 } else {
1734 struct NoOpenFolders;
1735
1736 workspace.show_notification(NotificationId::unique::<NoOpenFolders>(), cx, |cx| {
1737 cx.new(|cx| MessageNotification::new("This project has no folders open.", cx))
1738 })
1739 }
1740}
1741
1742fn open_telemetry_log_file(
1743 workspace: &mut Workspace,
1744 window: &mut Window,
1745 cx: &mut Context<Workspace>,
1746) {
1747 workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1748 let app_state = workspace.app_state().clone();
1749 cx.spawn_in(window, async move |workspace, cx| {
1750 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
1751 let path = client::telemetry::Telemetry::log_file_path();
1752 app_state.fs.load(&path).await.log_err()
1753 }
1754
1755 let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
1756
1757 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
1758 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
1759 if let Some(newline_offset) = log[start_offset..].find('\n') {
1760 start_offset += newline_offset + 1;
1761 }
1762 let log_suffix = &log[start_offset..];
1763 let header = concat!(
1764 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
1765 "// Telemetry can be disabled via the `settings.json` file.\n",
1766 "// Here is the data that has been reported for the current session:\n",
1767 );
1768 let content = format!("{}\n{}", header, log_suffix);
1769 let json = app_state.languages.language_for_name("JSON").await.log_err();
1770
1771 workspace.update_in( cx, |workspace, window, cx| {
1772 let project = workspace.project().clone();
1773 let buffer = project.update(cx, |project, cx| project.create_local_buffer(&content, json,false, cx));
1774 let buffer = cx.new(|cx| {
1775 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
1776 });
1777 workspace.add_item_to_active_pane(
1778 Box::new(cx.new(|cx| {
1779 let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
1780 editor.set_read_only(true);
1781 editor.set_breadcrumb_header("Telemetry Log".into());
1782 editor
1783 })),
1784 None,
1785 true,
1786 window, cx,
1787 );
1788 }).log_err()?;
1789
1790 Some(())
1791 })
1792 .detach();
1793 }).detach();
1794}
1795
1796fn open_bundled_file(
1797 workspace: &Workspace,
1798 text: Cow<'static, str>,
1799 title: &'static str,
1800 language: &'static str,
1801 window: &mut Window,
1802 cx: &mut Context<Workspace>,
1803) {
1804 let language = workspace.app_state().languages.language_for_name(language);
1805 cx.spawn_in(window, async move |workspace, cx| {
1806 let language = language.await.log_err();
1807 workspace
1808 .update_in(cx, |workspace, window, cx| {
1809 workspace.with_local_workspace(window, cx, |workspace, window, cx| {
1810 let project = workspace.project();
1811 let buffer = project.update(cx, move |project, cx| {
1812 let buffer =
1813 project.create_local_buffer(text.as_ref(), language, false, cx);
1814 buffer.update(cx, |buffer, cx| {
1815 buffer.set_capability(Capability::ReadOnly, cx);
1816 });
1817 buffer
1818 });
1819 let buffer =
1820 cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
1821 workspace.add_item_to_active_pane(
1822 Box::new(cx.new(|cx| {
1823 let mut editor =
1824 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1825 editor.set_read_only(true);
1826 editor.set_breadcrumb_header(title.into());
1827 editor
1828 })),
1829 None,
1830 true,
1831 window,
1832 cx,
1833 );
1834 })
1835 })?
1836 .await
1837 })
1838 .detach_and_log_err(cx);
1839}
1840
1841fn open_settings_file(
1842 abs_path: &'static Path,
1843 default_content: impl FnOnce() -> Rope + Send + 'static,
1844 window: &mut Window,
1845 cx: &mut Context<Workspace>,
1846) {
1847 cx.spawn_in(window, async move |workspace, cx| {
1848 let (worktree_creation_task, settings_open_task) = workspace
1849 .update_in(cx, |workspace, window, cx| {
1850 workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
1851 let worktree_creation_task = workspace.project().update(cx, |project, cx| {
1852 // Set up a dedicated worktree for settings, since
1853 // otherwise we're dropping and re-starting LSP servers
1854 // for each file inside on every settings file
1855 // close/open
1856
1857 // TODO: Do note that all other external files (e.g.
1858 // drag and drop from OS) still have their worktrees
1859 // released on file close, causing LSP servers'
1860 // restarts.
1861 project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
1862 });
1863 let settings_open_task =
1864 create_and_open_local_file(abs_path, window, cx, default_content);
1865 (worktree_creation_task, settings_open_task)
1866 })
1867 })?
1868 .await?;
1869 let _ = worktree_creation_task.await?;
1870 let _ = settings_open_task.await?;
1871 anyhow::Ok(())
1872 })
1873 .detach_and_log_err(cx);
1874}
1875
1876fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
1877 struct CaptureRecentAudioNotification {
1878 focus_handle: gpui::FocusHandle,
1879 save_result: Option<Result<(PathBuf, Duration), anyhow::Error>>,
1880 _save_task: Task<anyhow::Result<()>>,
1881 }
1882
1883 impl gpui::EventEmitter<DismissEvent> for CaptureRecentAudioNotification {}
1884 impl gpui::EventEmitter<SuppressEvent> for CaptureRecentAudioNotification {}
1885 impl gpui::Focusable for CaptureRecentAudioNotification {
1886 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
1887 self.focus_handle.clone()
1888 }
1889 }
1890 impl workspace::notifications::Notification for CaptureRecentAudioNotification {}
1891
1892 impl Render for CaptureRecentAudioNotification {
1893 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1894 let message = match &self.save_result {
1895 None => format!(
1896 "Saving up to {} seconds of recent audio",
1897 REPLAY_DURATION.as_secs(),
1898 ),
1899 Some(Ok((path, duration))) => format!(
1900 "Saved {} seconds of all audio to {}",
1901 duration.as_secs(),
1902 path.display(),
1903 ),
1904 Some(Err(e)) => format!("Error saving audio replays: {e:?}"),
1905 };
1906
1907 NotificationFrame::new()
1908 .with_title(Some("Saved Audio"))
1909 .show_suppress_button(false)
1910 .on_close(cx.listener(|_, _, _, cx| {
1911 cx.emit(DismissEvent);
1912 }))
1913 .with_content(message)
1914 }
1915 }
1916
1917 impl CaptureRecentAudioNotification {
1918 fn new(cx: &mut Context<Self>) -> Self {
1919 if AudioSettings::get_global(cx).rodio_audio {
1920 let executor = cx.background_executor().clone();
1921 let save_task = cx.default_global::<audio::Audio>().save_replays(executor);
1922 let _save_task = cx.spawn(async move |this, cx| {
1923 let res = save_task.await;
1924 this.update(cx, |this, cx| {
1925 this.save_result = Some(res);
1926 cx.notify();
1927 })
1928 });
1929
1930 Self {
1931 focus_handle: cx.focus_handle(),
1932 _save_task,
1933 save_result: None,
1934 }
1935 } else {
1936 Self {
1937 focus_handle: cx.focus_handle(),
1938 _save_task: Task::ready(Ok(())),
1939 save_result: Some(Err(anyhow::anyhow!(
1940 "Capturing recent audio is only supported on the experimental rodio audio pipeline"
1941 ))),
1942 }
1943 }
1944 }
1945 }
1946
1947 workspace.show_notification(
1948 NotificationId::unique::<CaptureRecentAudioNotification>(),
1949 cx,
1950 |cx| cx.new(CaptureRecentAudioNotification::new),
1951 );
1952}
1953
1954#[cfg(test)]
1955mod tests {
1956 use super::*;
1957 use assets::Assets;
1958 use collections::HashSet;
1959 use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow};
1960 use gpui::{
1961 Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
1962 TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
1963 };
1964 use language::{LanguageMatcher, LanguageRegistry};
1965 use pretty_assertions::{assert_eq, assert_ne};
1966 use project::{Project, ProjectPath};
1967 use serde_json::json;
1968 use settings::{SettingsStore, watch_config_file};
1969 use std::{
1970 path::{Path, PathBuf},
1971 time::Duration,
1972 };
1973 use theme::{ThemeRegistry, ThemeSettings};
1974 use util::{path, rel_path::rel_path};
1975 use workspace::{
1976 NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection,
1977 WorkspaceHandle,
1978 item::SaveOptions,
1979 item::{Item, ItemHandle},
1980 open_new, open_paths, pane,
1981 };
1982
1983 #[gpui::test]
1984 async fn test_open_non_existing_file(cx: &mut TestAppContext) {
1985 let app_state = init_test(cx);
1986 app_state
1987 .fs
1988 .as_fake()
1989 .insert_tree(
1990 path!("/root"),
1991 json!({
1992 "a": {
1993 },
1994 }),
1995 )
1996 .await;
1997
1998 cx.update(|cx| {
1999 open_paths(
2000 &[PathBuf::from(path!("/root/a/new"))],
2001 app_state.clone(),
2002 workspace::OpenOptions::default(),
2003 cx,
2004 )
2005 })
2006 .await
2007 .unwrap();
2008 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2009
2010 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
2011 workspace
2012 .update(cx, |workspace, _, cx| {
2013 assert!(workspace.active_item_as::<Editor>(cx).is_some())
2014 })
2015 .unwrap();
2016 }
2017
2018 #[gpui::test]
2019 async fn test_open_paths_action(cx: &mut TestAppContext) {
2020 let app_state = init_test(cx);
2021 app_state
2022 .fs
2023 .as_fake()
2024 .insert_tree(
2025 "/root",
2026 json!({
2027 "a": {
2028 "aa": null,
2029 "ab": null,
2030 },
2031 "b": {
2032 "ba": null,
2033 "bb": null,
2034 },
2035 "c": {
2036 "ca": null,
2037 "cb": null,
2038 },
2039 "d": {
2040 "da": null,
2041 "db": null,
2042 },
2043 "e": {
2044 "ea": null,
2045 "eb": null,
2046 }
2047 }),
2048 )
2049 .await;
2050
2051 cx.update(|cx| {
2052 open_paths(
2053 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
2054 app_state.clone(),
2055 workspace::OpenOptions::default(),
2056 cx,
2057 )
2058 })
2059 .await
2060 .unwrap();
2061 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2062
2063 cx.update(|cx| {
2064 open_paths(
2065 &[PathBuf::from("/root/a")],
2066 app_state.clone(),
2067 workspace::OpenOptions::default(),
2068 cx,
2069 )
2070 })
2071 .await
2072 .unwrap();
2073 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
2074 let workspace_1 = cx
2075 .read(|cx| cx.windows()[0].downcast::<Workspace>())
2076 .unwrap();
2077 cx.run_until_parked();
2078 workspace_1
2079 .update(cx, |workspace, window, cx| {
2080 assert_eq!(workspace.worktrees(cx).count(), 2);
2081 assert!(workspace.left_dock().read(cx).is_open());
2082 assert!(
2083 workspace
2084 .active_pane()
2085 .read(cx)
2086 .focus_handle(cx)
2087 .is_focused(window)
2088 );
2089 })
2090 .unwrap();
2091
2092 cx.update(|cx| {
2093 open_paths(
2094 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
2095 app_state.clone(),
2096 workspace::OpenOptions::default(),
2097 cx,
2098 )
2099 })
2100 .await
2101 .unwrap();
2102 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2103
2104 // Replace existing windows
2105 let window = cx
2106 .update(|cx| cx.windows()[0].downcast::<Workspace>())
2107 .unwrap();
2108 cx.update(|cx| {
2109 open_paths(
2110 &[PathBuf::from("/root/e")],
2111 app_state,
2112 workspace::OpenOptions {
2113 replace_window: Some(window),
2114 ..Default::default()
2115 },
2116 cx,
2117 )
2118 })
2119 .await
2120 .unwrap();
2121 cx.background_executor.run_until_parked();
2122 assert_eq!(cx.read(|cx| cx.windows().len()), 2);
2123 let workspace_1 = cx
2124 .update(|cx| cx.windows()[0].downcast::<Workspace>())
2125 .unwrap();
2126 workspace_1
2127 .update(cx, |workspace, window, cx| {
2128 assert_eq!(
2129 workspace
2130 .worktrees(cx)
2131 .map(|w| w.read(cx).abs_path())
2132 .collect::<Vec<_>>(),
2133 &[Path::new("/root/e").into()]
2134 );
2135 assert!(workspace.left_dock().read(cx).is_open());
2136 assert!(workspace.active_pane().focus_handle(cx).is_focused(window));
2137 })
2138 .unwrap();
2139 }
2140
2141 #[gpui::test]
2142 async fn test_open_add_new(cx: &mut TestAppContext) {
2143 let app_state = init_test(cx);
2144 app_state
2145 .fs
2146 .as_fake()
2147 .insert_tree(
2148 path!("/root"),
2149 json!({"a": "hey", "b": "", "dir": {"c": "f"}}),
2150 )
2151 .await;
2152
2153 cx.update(|cx| {
2154 open_paths(
2155 &[PathBuf::from(path!("/root/dir"))],
2156 app_state.clone(),
2157 workspace::OpenOptions::default(),
2158 cx,
2159 )
2160 })
2161 .await
2162 .unwrap();
2163 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2164
2165 cx.update(|cx| {
2166 open_paths(
2167 &[PathBuf::from(path!("/root/a"))],
2168 app_state.clone(),
2169 workspace::OpenOptions {
2170 open_new_workspace: Some(false),
2171 ..Default::default()
2172 },
2173 cx,
2174 )
2175 })
2176 .await
2177 .unwrap();
2178 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2179
2180 cx.update(|cx| {
2181 open_paths(
2182 &[PathBuf::from(path!("/root/dir/c"))],
2183 app_state.clone(),
2184 workspace::OpenOptions {
2185 open_new_workspace: Some(true),
2186 ..Default::default()
2187 },
2188 cx,
2189 )
2190 })
2191 .await
2192 .unwrap();
2193 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2194 }
2195
2196 #[gpui::test]
2197 async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) {
2198 let app_state = init_test(cx);
2199 app_state
2200 .fs
2201 .as_fake()
2202 .insert_tree(
2203 path!("/root"),
2204 json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}),
2205 )
2206 .await;
2207
2208 cx.update(|cx| {
2209 open_paths(
2210 &[PathBuf::from(path!("/root/dir1/a"))],
2211 app_state.clone(),
2212 workspace::OpenOptions::default(),
2213 cx,
2214 )
2215 })
2216 .await
2217 .unwrap();
2218 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2219 let window1 = cx.update(|cx| cx.active_window().unwrap());
2220
2221 cx.update(|cx| {
2222 open_paths(
2223 &[PathBuf::from(path!("/root/dir2/c"))],
2224 app_state.clone(),
2225 workspace::OpenOptions::default(),
2226 cx,
2227 )
2228 })
2229 .await
2230 .unwrap();
2231 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2232
2233 cx.update(|cx| {
2234 open_paths(
2235 &[PathBuf::from(path!("/root/dir2"))],
2236 app_state.clone(),
2237 workspace::OpenOptions::default(),
2238 cx,
2239 )
2240 })
2241 .await
2242 .unwrap();
2243 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2244 let window2 = cx.update(|cx| cx.active_window().unwrap());
2245 assert!(window1 != window2);
2246 cx.update_window(window1, |_, window, _| window.activate_window())
2247 .unwrap();
2248
2249 cx.update(|cx| {
2250 open_paths(
2251 &[PathBuf::from(path!("/root/dir2/c"))],
2252 app_state.clone(),
2253 workspace::OpenOptions::default(),
2254 cx,
2255 )
2256 })
2257 .await
2258 .unwrap();
2259 assert_eq!(cx.update(|cx| cx.windows().len()), 2);
2260 // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel)
2261 assert!(cx.update(|cx| cx.active_window().unwrap()) == window2);
2262 }
2263
2264 #[gpui::test]
2265 async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
2266 let executor = cx.executor();
2267 let app_state = init_test(cx);
2268
2269 cx.update(|cx| {
2270 SettingsStore::update_global(cx, |store, cx| {
2271 store.update_user_settings(cx, |settings| {
2272 settings
2273 .session
2274 .get_or_insert_default()
2275 .restore_unsaved_buffers = Some(false)
2276 });
2277 });
2278 });
2279
2280 app_state
2281 .fs
2282 .as_fake()
2283 .insert_tree(path!("/root"), json!({"a": "hey"}))
2284 .await;
2285
2286 cx.update(|cx| {
2287 open_paths(
2288 &[PathBuf::from(path!("/root/a"))],
2289 app_state.clone(),
2290 workspace::OpenOptions::default(),
2291 cx,
2292 )
2293 })
2294 .await
2295 .unwrap();
2296 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2297
2298 // When opening the workspace, the window is not in a edited state.
2299 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2300
2301 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2302 cx.update(|cx| window.read(cx).unwrap().is_edited())
2303 };
2304 let pane = window
2305 .read_with(cx, |workspace, _| workspace.active_pane().clone())
2306 .unwrap();
2307 let editor = window
2308 .read_with(cx, |workspace, cx| {
2309 workspace
2310 .active_item(cx)
2311 .unwrap()
2312 .downcast::<Editor>()
2313 .unwrap()
2314 })
2315 .unwrap();
2316
2317 assert!(!window_is_edited(window, cx));
2318
2319 // Editing a buffer marks the window as edited.
2320 window
2321 .update(cx, |_, window, cx| {
2322 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2323 })
2324 .unwrap();
2325
2326 assert!(window_is_edited(window, cx));
2327
2328 // Undoing the edit restores the window's edited state.
2329 window
2330 .update(cx, |_, window, cx| {
2331 editor.update(cx, |editor, cx| {
2332 editor.undo(&Default::default(), window, cx)
2333 });
2334 })
2335 .unwrap();
2336 assert!(!window_is_edited(window, cx));
2337
2338 // Redoing the edit marks the window as edited again.
2339 window
2340 .update(cx, |_, window, cx| {
2341 editor.update(cx, |editor, cx| {
2342 editor.redo(&Default::default(), window, cx)
2343 });
2344 })
2345 .unwrap();
2346 assert!(window_is_edited(window, cx));
2347 let weak = editor.downgrade();
2348
2349 // Closing the item restores the window's edited state.
2350 let close = window
2351 .update(cx, |_, window, cx| {
2352 pane.update(cx, |pane, cx| {
2353 drop(editor);
2354 pane.close_active_item(&Default::default(), window, cx)
2355 })
2356 })
2357 .unwrap();
2358 executor.run_until_parked();
2359
2360 cx.simulate_prompt_answer("Don't Save");
2361 close.await.unwrap();
2362
2363 // Advance the clock to ensure that the item has been serialized and dropped from the queue
2364 cx.executor().advance_clock(Duration::from_secs(1));
2365
2366 weak.assert_released();
2367 assert!(!window_is_edited(window, cx));
2368 // Opening the buffer again doesn't impact the window's edited state.
2369 cx.update(|cx| {
2370 open_paths(
2371 &[PathBuf::from(path!("/root/a"))],
2372 app_state,
2373 workspace::OpenOptions::default(),
2374 cx,
2375 )
2376 })
2377 .await
2378 .unwrap();
2379 executor.run_until_parked();
2380
2381 window
2382 .update(cx, |workspace, _, cx| {
2383 let editor = workspace
2384 .active_item(cx)
2385 .unwrap()
2386 .downcast::<Editor>()
2387 .unwrap();
2388
2389 editor.update(cx, |editor, cx| {
2390 assert_eq!(editor.text(cx), "hey");
2391 });
2392 })
2393 .unwrap();
2394
2395 let editor = window
2396 .read_with(cx, |workspace, cx| {
2397 workspace
2398 .active_item(cx)
2399 .unwrap()
2400 .downcast::<Editor>()
2401 .unwrap()
2402 })
2403 .unwrap();
2404 assert!(!window_is_edited(window, cx));
2405
2406 // Editing the buffer marks the window as edited.
2407 window
2408 .update(cx, |_, window, cx| {
2409 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2410 })
2411 .unwrap();
2412 executor.run_until_parked();
2413 assert!(window_is_edited(window, cx));
2414
2415 // Ensure closing the window via the mouse gets preempted due to the
2416 // buffer having unsaved changes.
2417 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2418 executor.run_until_parked();
2419 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2420
2421 // The window is successfully closed after the user dismisses the prompt.
2422 cx.simulate_prompt_answer("Don't Save");
2423 executor.run_until_parked();
2424 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2425 }
2426
2427 #[gpui::test]
2428 async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
2429 let app_state = init_test(cx);
2430 app_state
2431 .fs
2432 .as_fake()
2433 .insert_tree(path!("/root"), json!({"a": "hey"}))
2434 .await;
2435
2436 cx.update(|cx| {
2437 open_paths(
2438 &[PathBuf::from(path!("/root/a"))],
2439 app_state.clone(),
2440 workspace::OpenOptions::default(),
2441 cx,
2442 )
2443 })
2444 .await
2445 .unwrap();
2446
2447 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2448
2449 // When opening the workspace, the window is not in a edited state.
2450 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2451
2452 let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
2453 cx.update(|cx| window.read(cx).unwrap().is_edited())
2454 };
2455
2456 let editor = window
2457 .read_with(cx, |workspace, cx| {
2458 workspace
2459 .active_item(cx)
2460 .unwrap()
2461 .downcast::<Editor>()
2462 .unwrap()
2463 })
2464 .unwrap();
2465
2466 assert!(!window_is_edited(window, cx));
2467
2468 // Editing a buffer marks the window as edited.
2469 window
2470 .update(cx, |_, window, cx| {
2471 editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx));
2472 })
2473 .unwrap();
2474
2475 assert!(window_is_edited(window, cx));
2476 cx.run_until_parked();
2477
2478 // Advance the clock to make sure the workspace is serialized
2479 cx.executor().advance_clock(Duration::from_secs(1));
2480
2481 // When closing the window, no prompt shows up and the window is closed.
2482 // buffer having unsaved changes.
2483 assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
2484 cx.run_until_parked();
2485 assert_eq!(cx.update(|cx| cx.windows().len()), 0);
2486
2487 // When we now reopen the window, the edited state and the edited buffer are back
2488 cx.update(|cx| {
2489 open_paths(
2490 &[PathBuf::from(path!("/root/a"))],
2491 app_state.clone(),
2492 workspace::OpenOptions::default(),
2493 cx,
2494 )
2495 })
2496 .await
2497 .unwrap();
2498
2499 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2500 assert!(cx.update(|cx| cx.active_window().is_some()));
2501
2502 cx.run_until_parked();
2503
2504 // When opening the workspace, the window is not in a edited state.
2505 let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
2506 assert!(window_is_edited(window, cx));
2507
2508 window
2509 .update(cx, |workspace, _, cx| {
2510 let editor = workspace
2511 .active_item(cx)
2512 .unwrap()
2513 .downcast::<editor::Editor>()
2514 .unwrap();
2515 editor.update(cx, |editor, cx| {
2516 assert_eq!(editor.text(cx), "EDIThey");
2517 assert!(editor.is_dirty(cx));
2518 });
2519
2520 editor
2521 })
2522 .unwrap();
2523 }
2524
2525 #[gpui::test]
2526 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
2527 let app_state = init_test(cx);
2528 cx.update(|cx| {
2529 open_new(
2530 Default::default(),
2531 app_state.clone(),
2532 cx,
2533 |workspace, window, cx| {
2534 Editor::new_file(workspace, &Default::default(), window, cx)
2535 },
2536 )
2537 })
2538 .await
2539 .unwrap();
2540 cx.run_until_parked();
2541
2542 let workspace = cx
2543 .update(|cx| cx.windows().first().unwrap().downcast::<Workspace>())
2544 .unwrap();
2545
2546 let editor = workspace
2547 .update(cx, |workspace, _, cx| {
2548 let editor = workspace
2549 .active_item(cx)
2550 .unwrap()
2551 .downcast::<editor::Editor>()
2552 .unwrap();
2553 editor.update(cx, |editor, cx| {
2554 assert!(editor.text(cx).is_empty());
2555 assert!(!editor.is_dirty(cx));
2556 });
2557
2558 editor
2559 })
2560 .unwrap();
2561
2562 let save_task = workspace
2563 .update(cx, |workspace, window, cx| {
2564 workspace.save_active_item(SaveIntent::Save, window, cx)
2565 })
2566 .unwrap();
2567 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
2568 cx.background_executor.run_until_parked();
2569 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
2570 save_task.await.unwrap();
2571 workspace
2572 .update(cx, |_, _, cx| {
2573 editor.update(cx, |editor, cx| {
2574 assert!(!editor.is_dirty(cx));
2575 assert_eq!(editor.title(cx), "the-new-name");
2576 });
2577 })
2578 .unwrap();
2579 }
2580
2581 #[gpui::test]
2582 async fn test_open_entry(cx: &mut TestAppContext) {
2583 let app_state = init_test(cx);
2584 app_state
2585 .fs
2586 .as_fake()
2587 .insert_tree(
2588 path!("/root"),
2589 json!({
2590 "a": {
2591 "file1": "contents 1",
2592 "file2": "contents 2",
2593 "file3": "contents 3",
2594 },
2595 }),
2596 )
2597 .await;
2598
2599 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
2600 project.update(cx, |project, _cx| {
2601 project.languages().add(markdown_language())
2602 });
2603 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
2604 let workspace = window.root(cx).unwrap();
2605
2606 let entries = cx.read(|cx| workspace.file_project_paths(cx));
2607 let file1 = entries[0].clone();
2608 let file2 = entries[1].clone();
2609 let file3 = entries[2].clone();
2610
2611 // Open the first entry
2612 let entry_1 = window
2613 .update(cx, |w, window, cx| {
2614 w.open_path(file1.clone(), None, true, window, cx)
2615 })
2616 .unwrap()
2617 .await
2618 .unwrap();
2619 cx.read(|cx| {
2620 let pane = workspace.read(cx).active_pane().read(cx);
2621 assert_eq!(
2622 pane.active_item().unwrap().project_path(cx),
2623 Some(file1.clone())
2624 );
2625 assert_eq!(pane.items_len(), 1);
2626 });
2627
2628 // Open the second entry
2629 window
2630 .update(cx, |w, window, cx| {
2631 w.open_path(file2.clone(), None, true, window, cx)
2632 })
2633 .unwrap()
2634 .await
2635 .unwrap();
2636 cx.read(|cx| {
2637 let pane = workspace.read(cx).active_pane().read(cx);
2638 assert_eq!(
2639 pane.active_item().unwrap().project_path(cx),
2640 Some(file2.clone())
2641 );
2642 assert_eq!(pane.items_len(), 2);
2643 });
2644
2645 // Open the first entry again. The existing pane item is activated.
2646 let entry_1b = window
2647 .update(cx, |w, window, cx| {
2648 w.open_path(file1.clone(), None, true, window, cx)
2649 })
2650 .unwrap()
2651 .await
2652 .unwrap();
2653 assert_eq!(entry_1.item_id(), entry_1b.item_id());
2654
2655 cx.read(|cx| {
2656 let pane = workspace.read(cx).active_pane().read(cx);
2657 assert_eq!(
2658 pane.active_item().unwrap().project_path(cx),
2659 Some(file1.clone())
2660 );
2661 assert_eq!(pane.items_len(), 2);
2662 });
2663
2664 // Split the pane with the first entry, then open the second entry again.
2665 window
2666 .update(cx, |w, window, cx| {
2667 w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx);
2668 w.open_path(file2.clone(), None, true, window, cx)
2669 })
2670 .unwrap()
2671 .await
2672 .unwrap();
2673
2674 window
2675 .read_with(cx, |w, cx| {
2676 assert_eq!(
2677 w.active_pane()
2678 .read(cx)
2679 .active_item()
2680 .unwrap()
2681 .project_path(cx),
2682 Some(file2.clone())
2683 );
2684 })
2685 .unwrap();
2686
2687 // Open the third entry twice concurrently. Only one pane item is added.
2688 let (t1, t2) = window
2689 .update(cx, |w, window, cx| {
2690 (
2691 w.open_path(file3.clone(), None, true, window, cx),
2692 w.open_path(file3.clone(), None, true, window, cx),
2693 )
2694 })
2695 .unwrap();
2696 t1.await.unwrap();
2697 t2.await.unwrap();
2698 cx.read(|cx| {
2699 let pane = workspace.read(cx).active_pane().read(cx);
2700 assert_eq!(
2701 pane.active_item().unwrap().project_path(cx),
2702 Some(file3.clone())
2703 );
2704 let pane_entries = pane
2705 .items()
2706 .map(|i| i.project_path(cx).unwrap())
2707 .collect::<Vec<_>>();
2708 assert_eq!(pane_entries, &[file1, file2, file3]);
2709 });
2710 }
2711
2712 #[gpui::test]
2713 async fn test_open_paths(cx: &mut TestAppContext) {
2714 let app_state = init_test(cx);
2715
2716 app_state
2717 .fs
2718 .as_fake()
2719 .insert_tree(
2720 path!("/"),
2721 json!({
2722 "dir1": {
2723 "a.txt": ""
2724 },
2725 "dir2": {
2726 "b.txt": ""
2727 },
2728 "dir3": {
2729 "c.txt": ""
2730 },
2731 "d.txt": ""
2732 }),
2733 )
2734 .await;
2735
2736 cx.update(|cx| {
2737 open_paths(
2738 &[PathBuf::from(path!("/dir1/"))],
2739 app_state,
2740 workspace::OpenOptions::default(),
2741 cx,
2742 )
2743 })
2744 .await
2745 .unwrap();
2746 cx.run_until_parked();
2747 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
2748 let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
2749 let workspace = window.root(cx).unwrap();
2750
2751 #[track_caller]
2752 fn assert_project_panel_selection(
2753 workspace: &Workspace,
2754 expected_worktree_path: &Path,
2755 expected_entry_path: &RelPath,
2756 cx: &App,
2757 ) {
2758 let project_panel = [
2759 workspace.left_dock().read(cx).panel::<ProjectPanel>(),
2760 workspace.right_dock().read(cx).panel::<ProjectPanel>(),
2761 workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
2762 ]
2763 .into_iter()
2764 .find_map(std::convert::identity)
2765 .expect("found no project panels")
2766 .read(cx);
2767 let (selected_worktree, selected_entry) = project_panel
2768 .selected_entry(cx)
2769 .expect("project panel should have a selected entry");
2770 assert_eq!(
2771 selected_worktree.abs_path().as_ref(),
2772 expected_worktree_path,
2773 "Unexpected project panel selected worktree path"
2774 );
2775 assert_eq!(
2776 selected_entry.path.as_ref(),
2777 expected_entry_path,
2778 "Unexpected project panel selected entry path"
2779 );
2780 }
2781
2782 // Open a file within an existing worktree.
2783 window
2784 .update(cx, |workspace, window, cx| {
2785 workspace.open_paths(
2786 vec![path!("/dir1/a.txt").into()],
2787 OpenOptions {
2788 visible: Some(OpenVisible::All),
2789 ..Default::default()
2790 },
2791 None,
2792 window,
2793 cx,
2794 )
2795 })
2796 .unwrap()
2797 .await;
2798 cx.run_until_parked();
2799 cx.read(|cx| {
2800 let workspace = workspace.read(cx);
2801 assert_project_panel_selection(
2802 workspace,
2803 Path::new(path!("/dir1")),
2804 rel_path("a.txt"),
2805 cx,
2806 );
2807 assert_eq!(
2808 workspace
2809 .active_pane()
2810 .read(cx)
2811 .active_item()
2812 .unwrap()
2813 .act_as::<Editor>(cx)
2814 .unwrap()
2815 .read(cx)
2816 .title(cx),
2817 "a.txt"
2818 );
2819 });
2820
2821 // Open a file outside of any existing worktree.
2822 window
2823 .update(cx, |workspace, window, cx| {
2824 workspace.open_paths(
2825 vec![path!("/dir2/b.txt").into()],
2826 OpenOptions {
2827 visible: Some(OpenVisible::All),
2828 ..Default::default()
2829 },
2830 None,
2831 window,
2832 cx,
2833 )
2834 })
2835 .unwrap()
2836 .await;
2837 cx.run_until_parked();
2838 cx.read(|cx| {
2839 let workspace = workspace.read(cx);
2840 assert_project_panel_selection(
2841 workspace,
2842 Path::new(path!("/dir2/b.txt")),
2843 rel_path(""),
2844 cx,
2845 );
2846 let worktree_roots = workspace
2847 .worktrees(cx)
2848 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2849 .collect::<HashSet<_>>();
2850 assert_eq!(
2851 worktree_roots,
2852 vec![path!("/dir1"), path!("/dir2/b.txt")]
2853 .into_iter()
2854 .map(Path::new)
2855 .collect(),
2856 );
2857 assert_eq!(
2858 workspace
2859 .active_pane()
2860 .read(cx)
2861 .active_item()
2862 .unwrap()
2863 .act_as::<Editor>(cx)
2864 .unwrap()
2865 .read(cx)
2866 .title(cx),
2867 "b.txt"
2868 );
2869 });
2870
2871 // Ensure opening a directory and one of its children only adds one worktree.
2872 window
2873 .update(cx, |workspace, window, cx| {
2874 workspace.open_paths(
2875 vec![path!("/dir3").into(), path!("/dir3/c.txt").into()],
2876 OpenOptions {
2877 visible: Some(OpenVisible::All),
2878 ..Default::default()
2879 },
2880 None,
2881 window,
2882 cx,
2883 )
2884 })
2885 .unwrap()
2886 .await;
2887 cx.run_until_parked();
2888 cx.read(|cx| {
2889 let workspace = workspace.read(cx);
2890 assert_project_panel_selection(
2891 workspace,
2892 Path::new(path!("/dir3")),
2893 rel_path("c.txt"),
2894 cx,
2895 );
2896 let worktree_roots = workspace
2897 .worktrees(cx)
2898 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2899 .collect::<HashSet<_>>();
2900 assert_eq!(
2901 worktree_roots,
2902 vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2903 .into_iter()
2904 .map(Path::new)
2905 .collect(),
2906 );
2907 assert_eq!(
2908 workspace
2909 .active_pane()
2910 .read(cx)
2911 .active_item()
2912 .unwrap()
2913 .act_as::<Editor>(cx)
2914 .unwrap()
2915 .read(cx)
2916 .title(cx),
2917 "c.txt"
2918 );
2919 });
2920
2921 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
2922 window
2923 .update(cx, |workspace, window, cx| {
2924 workspace.open_paths(
2925 vec![path!("/d.txt").into()],
2926 OpenOptions {
2927 visible: Some(OpenVisible::None),
2928 ..Default::default()
2929 },
2930 None,
2931 window,
2932 cx,
2933 )
2934 })
2935 .unwrap()
2936 .await;
2937 cx.run_until_parked();
2938 cx.read(|cx| {
2939 let workspace = workspace.read(cx);
2940 assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx);
2941 let worktree_roots = workspace
2942 .worktrees(cx)
2943 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2944 .collect::<HashSet<_>>();
2945 assert_eq!(
2946 worktree_roots,
2947 vec![
2948 path!("/dir1"),
2949 path!("/dir2/b.txt"),
2950 path!("/dir3"),
2951 path!("/d.txt")
2952 ]
2953 .into_iter()
2954 .map(Path::new)
2955 .collect(),
2956 );
2957
2958 let visible_worktree_roots = workspace
2959 .visible_worktrees(cx)
2960 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
2961 .collect::<HashSet<_>>();
2962 assert_eq!(
2963 visible_worktree_roots,
2964 vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")]
2965 .into_iter()
2966 .map(Path::new)
2967 .collect(),
2968 );
2969
2970 assert_eq!(
2971 workspace
2972 .active_pane()
2973 .read(cx)
2974 .active_item()
2975 .unwrap()
2976 .act_as::<Editor>(cx)
2977 .unwrap()
2978 .read(cx)
2979 .title(cx),
2980 "d.txt"
2981 );
2982 });
2983 }
2984
2985 #[gpui::test]
2986 async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
2987 let app_state = init_test(cx);
2988 cx.update(|cx| {
2989 cx.update_global::<SettingsStore, _>(|store, cx| {
2990 store.update_user_settings(cx, |project_settings| {
2991 project_settings.project.worktree.file_scan_exclusions =
2992 Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
2993 });
2994 });
2995 });
2996 app_state
2997 .fs
2998 .as_fake()
2999 .insert_tree(
3000 path!("/root"),
3001 json!({
3002 ".gitignore": "ignored_dir\n",
3003 ".git": {
3004 "HEAD": "ref: refs/heads/main",
3005 },
3006 "regular_dir": {
3007 "file": "regular file contents",
3008 },
3009 "ignored_dir": {
3010 "ignored_subdir": {
3011 "file": "ignored subfile contents",
3012 },
3013 "file": "ignored file contents",
3014 },
3015 "excluded_dir": {
3016 "file": "excluded file contents",
3017 "ignored_subdir": {
3018 "file": "ignored subfile contents",
3019 },
3020 },
3021 }),
3022 )
3023 .await;
3024
3025 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3026 project.update(cx, |project, _cx| {
3027 project.languages().add(markdown_language())
3028 });
3029 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3030 let workspace = window.root(cx).unwrap();
3031
3032 let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
3033 let paths_to_open = [
3034 PathBuf::from(path!("/root/excluded_dir/file")),
3035 PathBuf::from(path!("/root/.git/HEAD")),
3036 PathBuf::from(path!("/root/excluded_dir/ignored_subdir")),
3037 ];
3038 let (opened_workspace, new_items) = cx
3039 .update(|cx| {
3040 workspace::open_paths(
3041 &paths_to_open,
3042 app_state,
3043 workspace::OpenOptions::default(),
3044 cx,
3045 )
3046 })
3047 .await
3048 .unwrap();
3049
3050 assert_eq!(
3051 opened_workspace.root(cx).unwrap().entity_id(),
3052 workspace.entity_id(),
3053 "Excluded files in subfolders of a workspace root should be opened in the workspace"
3054 );
3055 let mut opened_paths = cx.read(|cx| {
3056 assert_eq!(
3057 new_items.len(),
3058 paths_to_open.len(),
3059 "Expect to get the same number of opened items as submitted paths to open"
3060 );
3061 new_items
3062 .iter()
3063 .zip(paths_to_open.iter())
3064 .map(|(i, path)| {
3065 match i {
3066 Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)),
3067 Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
3068 None => None,
3069 }
3070 .flatten()
3071 })
3072 .collect::<Vec<_>>()
3073 });
3074 opened_paths.sort();
3075 assert_eq!(
3076 opened_paths,
3077 vec![
3078 None,
3079 Some(rel_path(".git/HEAD").into()),
3080 Some(rel_path("excluded_dir/file").into()),
3081 ],
3082 "Excluded files should get opened, excluded dir should not get opened"
3083 );
3084
3085 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3086 assert_eq!(
3087 initial_entries, entries,
3088 "Workspace entries should not change after opening excluded files and directories paths"
3089 );
3090
3091 cx.read(|cx| {
3092 let pane = workspace.read(cx).active_pane().read(cx);
3093 let mut opened_buffer_paths = pane
3094 .items()
3095 .map(|i| {
3096 i.project_path(cx)
3097 .expect("all excluded files that got open should have a path")
3098 .path
3099 })
3100 .collect::<Vec<_>>();
3101 opened_buffer_paths.sort();
3102 assert_eq!(
3103 opened_buffer_paths,
3104 vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()],
3105 "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
3106 );
3107 });
3108 }
3109
3110 #[gpui::test]
3111 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
3112 let app_state = init_test(cx);
3113 app_state
3114 .fs
3115 .as_fake()
3116 .insert_tree(path!("/root"), json!({ "a.txt": "" }))
3117 .await;
3118
3119 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3120 project.update(cx, |project, _cx| {
3121 project.languages().add(markdown_language())
3122 });
3123 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3124 let workspace = window.root(cx).unwrap();
3125
3126 // Open a file within an existing worktree.
3127 window
3128 .update(cx, |workspace, window, cx| {
3129 workspace.open_paths(
3130 vec![PathBuf::from(path!("/root/a.txt"))],
3131 OpenOptions {
3132 visible: Some(OpenVisible::All),
3133 ..Default::default()
3134 },
3135 None,
3136 window,
3137 cx,
3138 )
3139 })
3140 .unwrap()
3141 .await;
3142 let editor = cx.read(|cx| {
3143 let pane = workspace.read(cx).active_pane().read(cx);
3144 let item = pane.active_item().unwrap();
3145 item.downcast::<Editor>().unwrap()
3146 });
3147
3148 window
3149 .update(cx, |_, window, cx| {
3150 editor.update(cx, |editor, cx| editor.handle_input("x", window, cx));
3151 })
3152 .unwrap();
3153
3154 app_state
3155 .fs
3156 .as_fake()
3157 .insert_file(path!("/root/a.txt"), b"changed".to_vec())
3158 .await;
3159
3160 cx.run_until_parked();
3161 cx.read(|cx| assert!(editor.is_dirty(cx)));
3162 cx.read(|cx| assert!(editor.has_conflict(cx)));
3163
3164 let save_task = window
3165 .update(cx, |workspace, window, cx| {
3166 workspace.save_active_item(SaveIntent::Save, window, cx)
3167 })
3168 .unwrap();
3169 cx.background_executor.run_until_parked();
3170 cx.simulate_prompt_answer("Overwrite");
3171 save_task.await.unwrap();
3172 window
3173 .update(cx, |_, _, cx| {
3174 editor.update(cx, |editor, cx| {
3175 assert!(!editor.is_dirty(cx));
3176 assert!(!editor.has_conflict(cx));
3177 });
3178 })
3179 .unwrap();
3180 }
3181
3182 #[gpui::test]
3183 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
3184 let app_state = init_test(cx);
3185 app_state
3186 .fs
3187 .create_dir(Path::new(path!("/root")))
3188 .await
3189 .unwrap();
3190
3191 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3192 project.update(cx, |project, _| {
3193 project.languages().add(markdown_language());
3194 project.languages().add(rust_lang());
3195 });
3196 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3197 let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap());
3198
3199 // Create a new untitled buffer
3200 cx.dispatch_action(window.into(), NewFile);
3201 let editor = window
3202 .read_with(cx, |workspace, cx| {
3203 workspace
3204 .active_item(cx)
3205 .unwrap()
3206 .downcast::<Editor>()
3207 .unwrap()
3208 })
3209 .unwrap();
3210
3211 window
3212 .update(cx, |_, window, cx| {
3213 editor.update(cx, |editor, cx| {
3214 assert!(!editor.is_dirty(cx));
3215 assert_eq!(editor.title(cx), "untitled");
3216 assert!(Arc::ptr_eq(
3217 &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3218 &languages::PLAIN_TEXT
3219 ));
3220 editor.handle_input("hi", window, cx);
3221 assert!(editor.is_dirty(cx));
3222 });
3223 })
3224 .unwrap();
3225
3226 // Save the buffer. This prompts for a filename.
3227 let save_task = window
3228 .update(cx, |workspace, window, cx| {
3229 workspace.save_active_item(SaveIntent::Save, window, cx)
3230 })
3231 .unwrap();
3232 cx.background_executor.run_until_parked();
3233 cx.simulate_new_path_selection(|parent_dir| {
3234 assert_eq!(parent_dir, Path::new(path!("/root")));
3235 Some(parent_dir.join("the-new-name.rs"))
3236 });
3237 cx.read(|cx| {
3238 assert!(editor.is_dirty(cx));
3239 assert_eq!(editor.read(cx).title(cx), "hi");
3240 });
3241
3242 // When the save completes, the buffer's title is updated and the language is assigned based
3243 // on the path.
3244 save_task.await.unwrap();
3245 window
3246 .update(cx, |_, _, cx| {
3247 editor.update(cx, |editor, cx| {
3248 assert!(!editor.is_dirty(cx));
3249 assert_eq!(editor.title(cx), "the-new-name.rs");
3250 assert_eq!(
3251 editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3252 "Rust".into()
3253 );
3254 });
3255 })
3256 .unwrap();
3257
3258 // Edit the file and save it again. This time, there is no filename prompt.
3259 window
3260 .update(cx, |_, window, cx| {
3261 editor.update(cx, |editor, cx| {
3262 editor.handle_input(" there", window, cx);
3263 assert!(editor.is_dirty(cx));
3264 });
3265 })
3266 .unwrap();
3267
3268 let save_task = window
3269 .update(cx, |workspace, window, cx| {
3270 workspace.save_active_item(SaveIntent::Save, window, cx)
3271 })
3272 .unwrap();
3273 save_task.await.unwrap();
3274
3275 assert!(!cx.did_prompt_for_new_path());
3276 window
3277 .update(cx, |_, _, cx| {
3278 editor.update(cx, |editor, cx| {
3279 assert!(!editor.is_dirty(cx));
3280 assert_eq!(editor.title(cx), "the-new-name.rs")
3281 });
3282 })
3283 .unwrap();
3284
3285 // Open the same newly-created file in another pane item. The new editor should reuse
3286 // the same buffer.
3287 cx.dispatch_action(window.into(), NewFile);
3288 window
3289 .update(cx, |workspace, window, cx| {
3290 workspace.split_and_clone(
3291 workspace.active_pane().clone(),
3292 SplitDirection::Right,
3293 window,
3294 cx,
3295 );
3296 workspace.open_path(
3297 (worktree.read(cx).id(), rel_path("the-new-name.rs")),
3298 None,
3299 true,
3300 window,
3301 cx,
3302 )
3303 })
3304 .unwrap()
3305 .await
3306 .unwrap();
3307 let editor2 = window
3308 .update(cx, |workspace, _, cx| {
3309 workspace
3310 .active_item(cx)
3311 .unwrap()
3312 .downcast::<Editor>()
3313 .unwrap()
3314 })
3315 .unwrap();
3316 cx.read(|cx| {
3317 assert_eq!(
3318 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
3319 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
3320 );
3321 })
3322 }
3323
3324 #[gpui::test]
3325 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
3326 let app_state = init_test(cx);
3327 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
3328
3329 let project = Project::test(app_state.fs.clone(), [], cx).await;
3330 project.update(cx, |project, _| {
3331 project.languages().add(rust_lang());
3332 project.languages().add(markdown_language());
3333 });
3334 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3335
3336 // Create a new untitled buffer
3337 cx.dispatch_action(window.into(), NewFile);
3338 let editor = window
3339 .read_with(cx, |workspace, cx| {
3340 workspace
3341 .active_item(cx)
3342 .unwrap()
3343 .downcast::<Editor>()
3344 .unwrap()
3345 })
3346 .unwrap();
3347 window
3348 .update(cx, |_, window, cx| {
3349 editor.update(cx, |editor, cx| {
3350 assert!(Arc::ptr_eq(
3351 &editor.buffer().read(cx).language_at(0, cx).unwrap(),
3352 &languages::PLAIN_TEXT
3353 ));
3354 editor.handle_input("hi", window, cx);
3355 assert!(editor.is_dirty(cx));
3356 });
3357 })
3358 .unwrap();
3359
3360 // Save the buffer. This prompts for a filename.
3361 let save_task = window
3362 .update(cx, |workspace, window, cx| {
3363 workspace.save_active_item(SaveIntent::Save, window, cx)
3364 })
3365 .unwrap();
3366 cx.background_executor.run_until_parked();
3367 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
3368 save_task.await.unwrap();
3369 // The buffer is not dirty anymore and the language is assigned based on the path.
3370 window
3371 .update(cx, |_, _, cx| {
3372 editor.update(cx, |editor, cx| {
3373 assert!(!editor.is_dirty(cx));
3374 assert_eq!(
3375 editor.buffer().read(cx).language_at(0, cx).unwrap().name(),
3376 "Rust".into()
3377 )
3378 });
3379 })
3380 .unwrap();
3381 }
3382
3383 #[gpui::test]
3384 async fn test_pane_actions(cx: &mut TestAppContext) {
3385 let app_state = init_test(cx);
3386 app_state
3387 .fs
3388 .as_fake()
3389 .insert_tree(
3390 path!("/root"),
3391 json!({
3392 "a": {
3393 "file1": "contents 1",
3394 "file2": "contents 2",
3395 "file3": "contents 3",
3396 },
3397 }),
3398 )
3399 .await;
3400
3401 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3402 project.update(cx, |project, _cx| {
3403 project.languages().add(markdown_language())
3404 });
3405 let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3406 let workspace = window.root(cx).unwrap();
3407
3408 let entries = cx.read(|cx| workspace.file_project_paths(cx));
3409 let file1 = entries[0].clone();
3410
3411 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
3412
3413 window
3414 .update(cx, |w, window, cx| {
3415 w.open_path(file1.clone(), None, true, window, cx)
3416 })
3417 .unwrap()
3418 .await
3419 .unwrap();
3420
3421 let (editor_1, buffer) = window
3422 .update(cx, |_, window, cx| {
3423 pane_1.update(cx, |pane_1, cx| {
3424 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
3425 assert_eq!(editor.project_path(cx), Some(file1.clone()));
3426 let buffer = editor.update(cx, |editor, cx| {
3427 editor.insert("dirt", window, cx);
3428 editor.buffer().downgrade()
3429 });
3430 (editor.downgrade(), buffer)
3431 })
3432 })
3433 .unwrap();
3434
3435 cx.dispatch_action(window.into(), pane::SplitRight);
3436 let editor_2 = cx.update(|cx| {
3437 let pane_2 = workspace.read(cx).active_pane().clone();
3438 assert_ne!(pane_1, pane_2);
3439
3440 let pane2_item = pane_2.read(cx).active_item().unwrap();
3441 assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
3442
3443 pane2_item.downcast::<Editor>().unwrap().downgrade()
3444 });
3445 cx.dispatch_action(
3446 window.into(),
3447 workspace::CloseActiveItem {
3448 save_intent: None,
3449 close_pinned: false,
3450 },
3451 );
3452
3453 cx.background_executor.run_until_parked();
3454 window
3455 .read_with(cx, |workspace, _| {
3456 assert_eq!(workspace.panes().len(), 1);
3457 assert_eq!(workspace.active_pane(), &pane_1);
3458 })
3459 .unwrap();
3460
3461 cx.dispatch_action(
3462 window.into(),
3463 workspace::CloseActiveItem {
3464 save_intent: None,
3465 close_pinned: false,
3466 },
3467 );
3468 cx.background_executor.run_until_parked();
3469 cx.simulate_prompt_answer("Don't Save");
3470 cx.background_executor.run_until_parked();
3471
3472 window
3473 .update(cx, |workspace, _, cx| {
3474 assert_eq!(workspace.panes().len(), 1);
3475 assert!(workspace.active_item(cx).is_none());
3476 })
3477 .unwrap();
3478
3479 cx.background_executor
3480 .advance_clock(SERIALIZATION_THROTTLE_TIME);
3481 cx.update(|_| {});
3482 editor_1.assert_released();
3483 editor_2.assert_released();
3484 buffer.assert_released();
3485 }
3486
3487 #[gpui::test]
3488 async fn test_navigation(cx: &mut TestAppContext) {
3489 let app_state = init_test(cx);
3490 app_state
3491 .fs
3492 .as_fake()
3493 .insert_tree(
3494 path!("/root"),
3495 json!({
3496 "a": {
3497 "file1": "contents 1\n".repeat(20),
3498 "file2": "contents 2\n".repeat(20),
3499 "file3": "contents 3\n".repeat(20),
3500 },
3501 }),
3502 )
3503 .await;
3504
3505 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3506 project.update(cx, |project, _cx| {
3507 project.languages().add(markdown_language())
3508 });
3509 let workspace =
3510 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
3511 let pane = workspace
3512 .read_with(cx, |workspace, _| workspace.active_pane().clone())
3513 .unwrap();
3514
3515 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3516 let file1 = entries[0].clone();
3517 let file2 = entries[1].clone();
3518 let file3 = entries[2].clone();
3519
3520 let editor1 = workspace
3521 .update(cx, |w, window, cx| {
3522 w.open_path(file1.clone(), None, true, window, cx)
3523 })
3524 .unwrap()
3525 .await
3526 .unwrap()
3527 .downcast::<Editor>()
3528 .unwrap();
3529 workspace
3530 .update(cx, |_, window, cx| {
3531 editor1.update(cx, |editor, cx| {
3532 editor.change_selections(Default::default(), window, cx, |s| {
3533 s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
3534 ..DisplayPoint::new(DisplayRow(10), 0)])
3535 });
3536 });
3537 })
3538 .unwrap();
3539
3540 let editor2 = workspace
3541 .update(cx, |w, window, cx| {
3542 w.open_path(file2.clone(), None, true, window, cx)
3543 })
3544 .unwrap()
3545 .await
3546 .unwrap()
3547 .downcast::<Editor>()
3548 .unwrap();
3549 let editor3 = workspace
3550 .update(cx, |w, window, cx| {
3551 w.open_path(file3.clone(), None, true, window, cx)
3552 })
3553 .unwrap()
3554 .await
3555 .unwrap()
3556 .downcast::<Editor>()
3557 .unwrap();
3558
3559 workspace
3560 .update(cx, |_, window, cx| {
3561 editor3.update(cx, |editor, cx| {
3562 editor.change_selections(Default::default(), window, cx, |s| {
3563 s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
3564 ..DisplayPoint::new(DisplayRow(12), 0)])
3565 });
3566 editor.newline(&Default::default(), window, cx);
3567 editor.newline(&Default::default(), window, cx);
3568 editor.move_down(&Default::default(), window, cx);
3569 editor.move_down(&Default::default(), window, cx);
3570 editor.save(
3571 SaveOptions {
3572 format: true,
3573 autosave: false,
3574 },
3575 project.clone(),
3576 window,
3577 cx,
3578 )
3579 })
3580 })
3581 .unwrap()
3582 .await
3583 .unwrap();
3584 workspace
3585 .update(cx, |_, window, cx| {
3586 editor3.update(cx, |editor, cx| {
3587 editor.set_scroll_position(point(0., 12.5), window, cx)
3588 });
3589 })
3590 .unwrap();
3591 assert_eq!(
3592 active_location(&workspace, cx),
3593 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3594 );
3595
3596 workspace
3597 .update(cx, |w, window, cx| {
3598 w.go_back(w.active_pane().downgrade(), window, cx)
3599 })
3600 .unwrap()
3601 .await
3602 .unwrap();
3603 assert_eq!(
3604 active_location(&workspace, cx),
3605 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3606 );
3607
3608 workspace
3609 .update(cx, |w, window, cx| {
3610 w.go_back(w.active_pane().downgrade(), window, cx)
3611 })
3612 .unwrap()
3613 .await
3614 .unwrap();
3615 assert_eq!(
3616 active_location(&workspace, cx),
3617 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3618 );
3619
3620 workspace
3621 .update(cx, |w, window, cx| {
3622 w.go_back(w.active_pane().downgrade(), window, cx)
3623 })
3624 .unwrap()
3625 .await
3626 .unwrap();
3627 assert_eq!(
3628 active_location(&workspace, cx),
3629 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3630 );
3631
3632 workspace
3633 .update(cx, |w, window, cx| {
3634 w.go_back(w.active_pane().downgrade(), window, cx)
3635 })
3636 .unwrap()
3637 .await
3638 .unwrap();
3639 assert_eq!(
3640 active_location(&workspace, cx),
3641 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3642 );
3643
3644 // Go back one more time and ensure we don't navigate past the first item in the history.
3645 workspace
3646 .update(cx, |w, window, cx| {
3647 w.go_back(w.active_pane().downgrade(), window, cx)
3648 })
3649 .unwrap()
3650 .await
3651 .unwrap();
3652 assert_eq!(
3653 active_location(&workspace, cx),
3654 (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3655 );
3656
3657 workspace
3658 .update(cx, |w, window, cx| {
3659 w.go_forward(w.active_pane().downgrade(), window, cx)
3660 })
3661 .unwrap()
3662 .await
3663 .unwrap();
3664 assert_eq!(
3665 active_location(&workspace, cx),
3666 (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.)
3667 );
3668
3669 workspace
3670 .update(cx, |w, window, cx| {
3671 w.go_forward(w.active_pane().downgrade(), window, cx)
3672 })
3673 .unwrap()
3674 .await
3675 .unwrap();
3676 assert_eq!(
3677 active_location(&workspace, cx),
3678 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3679 );
3680
3681 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
3682 // location.
3683 workspace
3684 .update(cx, |_, window, cx| {
3685 pane.update(cx, |pane, cx| {
3686 let editor3_id = editor3.entity_id();
3687 drop(editor3);
3688 pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx)
3689 })
3690 })
3691 .unwrap()
3692 .await
3693 .unwrap();
3694 workspace
3695 .update(cx, |w, window, cx| {
3696 w.go_forward(w.active_pane().downgrade(), window, cx)
3697 })
3698 .unwrap()
3699 .await
3700 .unwrap();
3701 assert_eq!(
3702 active_location(&workspace, cx),
3703 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3704 );
3705
3706 workspace
3707 .update(cx, |w, window, cx| {
3708 w.go_forward(w.active_pane().downgrade(), window, cx)
3709 })
3710 .unwrap()
3711 .await
3712 .unwrap();
3713 assert_eq!(
3714 active_location(&workspace, cx),
3715 (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5)
3716 );
3717
3718 workspace
3719 .update(cx, |w, window, cx| {
3720 w.go_back(w.active_pane().downgrade(), window, cx)
3721 })
3722 .unwrap()
3723 .await
3724 .unwrap();
3725 assert_eq!(
3726 active_location(&workspace, cx),
3727 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3728 );
3729
3730 // Go back to an item that has been closed and removed from disk
3731 workspace
3732 .update(cx, |_, window, cx| {
3733 pane.update(cx, |pane, cx| {
3734 let editor2_id = editor2.entity_id();
3735 drop(editor2);
3736 pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx)
3737 })
3738 })
3739 .unwrap()
3740 .await
3741 .unwrap();
3742 app_state
3743 .fs
3744 .remove_file(Path::new(path!("/root/a/file2")), Default::default())
3745 .await
3746 .unwrap();
3747 cx.background_executor.run_until_parked();
3748
3749 workspace
3750 .update(cx, |w, window, cx| {
3751 w.go_back(w.active_pane().downgrade(), window, cx)
3752 })
3753 .unwrap()
3754 .await
3755 .unwrap();
3756 assert_eq!(
3757 active_location(&workspace, cx),
3758 (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3759 );
3760 workspace
3761 .update(cx, |w, window, cx| {
3762 w.go_forward(w.active_pane().downgrade(), window, cx)
3763 })
3764 .unwrap()
3765 .await
3766 .unwrap();
3767 assert_eq!(
3768 active_location(&workspace, cx),
3769 (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.)
3770 );
3771
3772 // Modify file to collapse multiple nav history entries into the same location.
3773 // Ensure we don't visit the same location twice when navigating.
3774 workspace
3775 .update(cx, |_, window, cx| {
3776 editor1.update(cx, |editor, cx| {
3777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3778 s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
3779 ..DisplayPoint::new(DisplayRow(15), 0)])
3780 })
3781 });
3782 })
3783 .unwrap();
3784 for _ in 0..5 {
3785 workspace
3786 .update(cx, |_, window, cx| {
3787 editor1.update(cx, |editor, cx| {
3788 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3789 s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
3790 ..DisplayPoint::new(DisplayRow(3), 0)])
3791 });
3792 });
3793 })
3794 .unwrap();
3795
3796 workspace
3797 .update(cx, |_, window, cx| {
3798 editor1.update(cx, |editor, cx| {
3799 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3800 s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
3801 ..DisplayPoint::new(DisplayRow(13), 0)])
3802 })
3803 });
3804 })
3805 .unwrap();
3806 }
3807 workspace
3808 .update(cx, |_, window, cx| {
3809 editor1.update(cx, |editor, cx| {
3810 editor.transact(window, cx, |editor, window, cx| {
3811 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3812 s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
3813 ..DisplayPoint::new(DisplayRow(14), 0)])
3814 });
3815 editor.insert("", window, cx);
3816 })
3817 });
3818 })
3819 .unwrap();
3820
3821 workspace
3822 .update(cx, |_, window, cx| {
3823 editor1.update(cx, |editor, cx| {
3824 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3825 s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
3826 ..DisplayPoint::new(DisplayRow(1), 0)])
3827 })
3828 });
3829 })
3830 .unwrap();
3831 workspace
3832 .update(cx, |w, window, cx| {
3833 w.go_back(w.active_pane().downgrade(), window, cx)
3834 })
3835 .unwrap()
3836 .await
3837 .unwrap();
3838 assert_eq!(
3839 active_location(&workspace, cx),
3840 (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.)
3841 );
3842 workspace
3843 .update(cx, |w, window, cx| {
3844 w.go_back(w.active_pane().downgrade(), window, cx)
3845 })
3846 .unwrap()
3847 .await
3848 .unwrap();
3849 assert_eq!(
3850 active_location(&workspace, cx),
3851 (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.)
3852 );
3853
3854 fn active_location(
3855 workspace: &WindowHandle<Workspace>,
3856 cx: &mut TestAppContext,
3857 ) -> (ProjectPath, DisplayPoint, f32) {
3858 workspace
3859 .update(cx, |workspace, _, cx| {
3860 let item = workspace.active_item(cx).unwrap();
3861 let editor = item.downcast::<Editor>().unwrap();
3862 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
3863 (
3864 editor.selections.display_ranges(cx),
3865 editor.scroll_position(cx),
3866 )
3867 });
3868 (
3869 item.project_path(cx).unwrap(),
3870 selections[0].start,
3871 scroll_position.y,
3872 )
3873 })
3874 .unwrap()
3875 }
3876 }
3877
3878 #[gpui::test]
3879 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
3880 let app_state = init_test(cx);
3881 app_state
3882 .fs
3883 .as_fake()
3884 .insert_tree(
3885 path!("/root"),
3886 json!({
3887 "a": {
3888 "file1": "",
3889 "file2": "",
3890 "file3": "",
3891 "file4": "",
3892 },
3893 }),
3894 )
3895 .await;
3896
3897 let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await;
3898 project.update(cx, |project, _cx| {
3899 project.languages().add(markdown_language())
3900 });
3901 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
3902 let pane = workspace
3903 .read_with(cx, |workspace, _| workspace.active_pane().clone())
3904 .unwrap();
3905
3906 let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx));
3907 let file1 = entries[0].clone();
3908 let file2 = entries[1].clone();
3909 let file3 = entries[2].clone();
3910 let file4 = entries[3].clone();
3911
3912 let file1_item_id = workspace
3913 .update(cx, |w, window, cx| {
3914 w.open_path(file1.clone(), None, true, window, cx)
3915 })
3916 .unwrap()
3917 .await
3918 .unwrap()
3919 .item_id();
3920 let file2_item_id = workspace
3921 .update(cx, |w, window, cx| {
3922 w.open_path(file2.clone(), None, true, window, cx)
3923 })
3924 .unwrap()
3925 .await
3926 .unwrap()
3927 .item_id();
3928 let file3_item_id = workspace
3929 .update(cx, |w, window, cx| {
3930 w.open_path(file3.clone(), None, true, window, cx)
3931 })
3932 .unwrap()
3933 .await
3934 .unwrap()
3935 .item_id();
3936 let file4_item_id = workspace
3937 .update(cx, |w, window, cx| {
3938 w.open_path(file4.clone(), None, true, window, cx)
3939 })
3940 .unwrap()
3941 .await
3942 .unwrap()
3943 .item_id();
3944 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3945
3946 // Close all the pane items in some arbitrary order.
3947 workspace
3948 .update(cx, |_, window, cx| {
3949 pane.update(cx, |pane, cx| {
3950 pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx)
3951 })
3952 })
3953 .unwrap()
3954 .await
3955 .unwrap();
3956 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
3957
3958 workspace
3959 .update(cx, |_, window, cx| {
3960 pane.update(cx, |pane, cx| {
3961 pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx)
3962 })
3963 })
3964 .unwrap()
3965 .await
3966 .unwrap();
3967 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3968
3969 workspace
3970 .update(cx, |_, window, cx| {
3971 pane.update(cx, |pane, cx| {
3972 pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx)
3973 })
3974 })
3975 .unwrap()
3976 .await
3977 .unwrap();
3978 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3979 workspace
3980 .update(cx, |_, window, cx| {
3981 pane.update(cx, |pane, cx| {
3982 pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx)
3983 })
3984 })
3985 .unwrap()
3986 .await
3987 .unwrap();
3988
3989 assert_eq!(active_path(&workspace, cx), None);
3990
3991 // Reopen all the closed items, ensuring they are reopened in the same order
3992 // in which they were closed.
3993 workspace
3994 .update(cx, Workspace::reopen_closed_item)
3995 .unwrap()
3996 .await
3997 .unwrap();
3998 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
3999
4000 workspace
4001 .update(cx, Workspace::reopen_closed_item)
4002 .unwrap()
4003 .await
4004 .unwrap();
4005 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4006
4007 workspace
4008 .update(cx, Workspace::reopen_closed_item)
4009 .unwrap()
4010 .await
4011 .unwrap();
4012 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4013
4014 workspace
4015 .update(cx, Workspace::reopen_closed_item)
4016 .unwrap()
4017 .await
4018 .unwrap();
4019 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4020
4021 // Reopening past the last closed item is a no-op.
4022 workspace
4023 .update(cx, Workspace::reopen_closed_item)
4024 .unwrap()
4025 .await
4026 .unwrap();
4027 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4028
4029 // Reopening closed items doesn't interfere with navigation history.
4030 workspace
4031 .update(cx, |workspace, window, cx| {
4032 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4033 })
4034 .unwrap()
4035 .await
4036 .unwrap();
4037 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4038
4039 workspace
4040 .update(cx, |workspace, window, cx| {
4041 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4042 })
4043 .unwrap()
4044 .await
4045 .unwrap();
4046 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4047
4048 workspace
4049 .update(cx, |workspace, window, cx| {
4050 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4051 })
4052 .unwrap()
4053 .await
4054 .unwrap();
4055 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4056
4057 workspace
4058 .update(cx, |workspace, window, cx| {
4059 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4060 })
4061 .unwrap()
4062 .await
4063 .unwrap();
4064 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
4065
4066 workspace
4067 .update(cx, |workspace, window, cx| {
4068 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4069 })
4070 .unwrap()
4071 .await
4072 .unwrap();
4073 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
4074
4075 workspace
4076 .update(cx, |workspace, window, cx| {
4077 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4078 })
4079 .unwrap()
4080 .await
4081 .unwrap();
4082 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
4083
4084 workspace
4085 .update(cx, |workspace, window, cx| {
4086 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4087 })
4088 .unwrap()
4089 .await
4090 .unwrap();
4091 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4092
4093 workspace
4094 .update(cx, |workspace, window, cx| {
4095 workspace.go_back(workspace.active_pane().downgrade(), window, cx)
4096 })
4097 .unwrap()
4098 .await
4099 .unwrap();
4100 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
4101
4102 fn active_path(
4103 workspace: &WindowHandle<Workspace>,
4104 cx: &TestAppContext,
4105 ) -> Option<ProjectPath> {
4106 workspace
4107 .read_with(cx, |workspace, cx| {
4108 let item = workspace.active_item(cx)?;
4109 item.project_path(cx)
4110 })
4111 .unwrap()
4112 }
4113 }
4114
4115 fn init_keymap_test(cx: &mut TestAppContext) -> Arc<AppState> {
4116 cx.update(|cx| {
4117 let app_state = AppState::test(cx);
4118
4119 theme::init(theme::LoadThemes::JustBase, cx);
4120 client::init(&app_state.client, cx);
4121 language::init(cx);
4122 workspace::init(app_state.clone(), cx);
4123 onboarding::init(cx);
4124 Project::init_settings(cx);
4125 app_state
4126 })
4127 }
4128
4129 actions!(test_only, [ActionA, ActionB]);
4130
4131 #[gpui::test]
4132 async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
4133 let executor = cx.executor();
4134 let app_state = init_keymap_test(cx);
4135 let project = Project::test(app_state.fs.clone(), [], cx).await;
4136 let workspace =
4137 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4138
4139 // From the Atom keymap
4140 use workspace::ActivatePreviousPane;
4141 // From the JetBrains keymap
4142 use workspace::ActivatePreviousItem;
4143
4144 app_state
4145 .fs
4146 .save(
4147 "/settings.json".as_ref(),
4148 &r#"{"base_keymap": "Atom"}"#.into(),
4149 Default::default(),
4150 )
4151 .await
4152 .unwrap();
4153
4154 app_state
4155 .fs
4156 .save(
4157 "/keymap.json".as_ref(),
4158 &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4159 Default::default(),
4160 )
4161 .await
4162 .unwrap();
4163 executor.run_until_parked();
4164 cx.update(|cx| {
4165 let settings_rx = watch_config_file(
4166 &executor,
4167 app_state.fs.clone(),
4168 PathBuf::from("/settings.json"),
4169 );
4170 let keymap_rx = watch_config_file(
4171 &executor,
4172 app_state.fs.clone(),
4173 PathBuf::from("/keymap.json"),
4174 );
4175 let global_settings_rx = watch_config_file(
4176 &executor,
4177 app_state.fs.clone(),
4178 PathBuf::from("/global_settings.json"),
4179 );
4180 handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4181 handle_keymap_file_changes(keymap_rx, cx);
4182 });
4183 workspace
4184 .update(cx, |workspace, _, cx| {
4185 workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4186 workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4187 workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {});
4188 workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {});
4189 cx.notify();
4190 })
4191 .unwrap();
4192 executor.run_until_parked();
4193 // Test loading the keymap base at all
4194 assert_key_bindings_for(
4195 workspace.into(),
4196 cx,
4197 vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4198 line!(),
4199 );
4200
4201 // Test modifying the users keymap, while retaining the base keymap
4202 app_state
4203 .fs
4204 .save(
4205 "/keymap.json".as_ref(),
4206 &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(),
4207 Default::default(),
4208 )
4209 .await
4210 .unwrap();
4211
4212 executor.run_until_parked();
4213
4214 assert_key_bindings_for(
4215 workspace.into(),
4216 cx,
4217 vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)],
4218 line!(),
4219 );
4220
4221 // Test modifying the base, while retaining the users keymap
4222 app_state
4223 .fs
4224 .save(
4225 "/settings.json".as_ref(),
4226 &r#"{"base_keymap": "JetBrains"}"#.into(),
4227 Default::default(),
4228 )
4229 .await
4230 .unwrap();
4231
4232 executor.run_until_parked();
4233
4234 assert_key_bindings_for(
4235 workspace.into(),
4236 cx,
4237 vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)],
4238 line!(),
4239 );
4240 }
4241
4242 #[gpui::test]
4243 async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
4244 let executor = cx.executor();
4245 let app_state = init_keymap_test(cx);
4246 let project = Project::test(app_state.fs.clone(), [], cx).await;
4247 let workspace =
4248 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4249
4250 // From the Atom keymap
4251 use workspace::ActivatePreviousPane;
4252 // From the JetBrains keymap
4253 use diagnostics::Deploy;
4254
4255 workspace
4256 .update(cx, |workspace, _, _| {
4257 workspace.register_action(|_, _: &ActionA, _window, _cx| {});
4258 workspace.register_action(|_, _: &ActionB, _window, _cx| {});
4259 workspace.register_action(|_, _: &Deploy, _window, _cx| {});
4260 })
4261 .unwrap();
4262 app_state
4263 .fs
4264 .save(
4265 "/settings.json".as_ref(),
4266 &r#"{"base_keymap": "Atom"}"#.into(),
4267 Default::default(),
4268 )
4269 .await
4270 .unwrap();
4271 app_state
4272 .fs
4273 .save(
4274 "/keymap.json".as_ref(),
4275 &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(),
4276 Default::default(),
4277 )
4278 .await
4279 .unwrap();
4280
4281 cx.update(|cx| {
4282 let settings_rx = watch_config_file(
4283 &executor,
4284 app_state.fs.clone(),
4285 PathBuf::from("/settings.json"),
4286 );
4287 let keymap_rx = watch_config_file(
4288 &executor,
4289 app_state.fs.clone(),
4290 PathBuf::from("/keymap.json"),
4291 );
4292
4293 let global_settings_rx = watch_config_file(
4294 &executor,
4295 app_state.fs.clone(),
4296 PathBuf::from("/global_settings.json"),
4297 );
4298 handle_settings_file_changes(settings_rx, global_settings_rx, cx, |_, _| {});
4299 handle_keymap_file_changes(keymap_rx, cx);
4300 });
4301
4302 cx.background_executor.run_until_parked();
4303
4304 cx.background_executor.run_until_parked();
4305 // Test loading the keymap base at all
4306 assert_key_bindings_for(
4307 workspace.into(),
4308 cx,
4309 vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)],
4310 line!(),
4311 );
4312
4313 // Test disabling the key binding for the base keymap
4314 app_state
4315 .fs
4316 .save(
4317 "/keymap.json".as_ref(),
4318 &r#"[{"bindings": {"backspace": null}}]"#.into(),
4319 Default::default(),
4320 )
4321 .await
4322 .unwrap();
4323
4324 cx.background_executor.run_until_parked();
4325
4326 assert_key_bindings_for(
4327 workspace.into(),
4328 cx,
4329 vec![("k", &ActivatePreviousPane)],
4330 line!(),
4331 );
4332
4333 // Test modifying the base, while retaining the users keymap
4334 app_state
4335 .fs
4336 .save(
4337 "/settings.json".as_ref(),
4338 &r#"{"base_keymap": "JetBrains"}"#.into(),
4339 Default::default(),
4340 )
4341 .await
4342 .unwrap();
4343
4344 cx.background_executor.run_until_parked();
4345
4346 assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!());
4347 }
4348
4349 #[gpui::test]
4350 async fn test_generate_keymap_json_schema_for_registered_actions(
4351 cx: &mut gpui::TestAppContext,
4352 ) {
4353 init_keymap_test(cx);
4354 cx.update(|cx| {
4355 // Make sure it doesn't panic.
4356 KeymapFile::generate_json_schema_for_registered_actions(cx);
4357 });
4358 }
4359
4360 /// Actions that don't build from empty input won't work from command palette invocation.
4361 #[gpui::test]
4362 async fn test_actions_build_with_empty_input(cx: &mut gpui::TestAppContext) {
4363 init_keymap_test(cx);
4364 cx.update(|cx| {
4365 let all_actions = cx.all_action_names();
4366 let mut failing_names = Vec::new();
4367 let mut errors = Vec::new();
4368 for action in all_actions {
4369 match action.to_string().as_str() {
4370 "vim::FindCommand"
4371 | "vim::Literal"
4372 | "vim::ResizePane"
4373 | "vim::PushObject"
4374 | "vim::PushFindForward"
4375 | "vim::PushFindBackward"
4376 | "vim::PushSneak"
4377 | "vim::PushSneakBackward"
4378 | "vim::PushChangeSurrounds"
4379 | "vim::PushJump"
4380 | "vim::PushDigraph"
4381 | "vim::PushLiteral"
4382 | "vim::PushHelixNext"
4383 | "vim::PushHelixPrevious"
4384 | "vim::Number"
4385 | "vim::SelectRegister"
4386 | "git::StageAndNext"
4387 | "git::UnstageAndNext"
4388 | "terminal::SendText"
4389 | "terminal::SendKeystroke"
4390 | "app_menu::OpenApplicationMenu"
4391 | "picker::ConfirmInput"
4392 | "editor::HandleInput"
4393 | "editor::FoldAtLevel"
4394 | "pane::ActivateItem"
4395 | "workspace::ActivatePane"
4396 | "workspace::MoveItemToPane"
4397 | "workspace::MoveItemToPaneInDirection"
4398 | "workspace::OpenTerminal"
4399 | "workspace::SendKeystrokes"
4400 | "agent::NewNativeAgentThreadFromSummary"
4401 | "action::Sequence"
4402 | "zed::OpenBrowser"
4403 | "zed::OpenZedUrl" => {}
4404 _ => {
4405 let result = cx.build_action(action, None);
4406 match &result {
4407 Ok(_) => {}
4408 Err(err) => {
4409 failing_names.push(action);
4410 errors.push(format!("{action} failed to build: {err:?}"));
4411 }
4412 }
4413 }
4414 }
4415 }
4416 if !errors.is_empty() {
4417 panic!(
4418 "Failed to build actions using {{}} as input: {:?}. Errors:\n{}",
4419 failing_names,
4420 errors.join("\n")
4421 );
4422 }
4423 });
4424 }
4425
4426 /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos
4427 /// and let you know when introducing a new namespace.
4428 #[gpui::test]
4429 async fn test_action_namespaces(cx: &mut gpui::TestAppContext) {
4430 use itertools::Itertools;
4431
4432 init_keymap_test(cx);
4433 cx.update(|cx| {
4434 let all_actions = cx.all_action_names();
4435
4436 let mut actions_without_namespace = Vec::new();
4437 let all_namespaces = all_actions
4438 .iter()
4439 .filter_map(|action_name| {
4440 let namespace = action_name
4441 .split("::")
4442 .collect::<Vec<_>>()
4443 .into_iter()
4444 .rev()
4445 .skip(1)
4446 .rev()
4447 .join("::");
4448 if namespace.is_empty() {
4449 actions_without_namespace.push(*action_name);
4450 }
4451 if &namespace == "test_only" || &namespace == "stories" {
4452 None
4453 } else {
4454 Some(namespace)
4455 }
4456 })
4457 .sorted()
4458 .dedup()
4459 .collect::<Vec<_>>();
4460 assert_eq!(actions_without_namespace, Vec::<&str>::new());
4461
4462 let expected_namespaces = vec![
4463 "action",
4464 "activity_indicator",
4465 "agent",
4466 #[cfg(not(target_os = "macos"))]
4467 "app_menu",
4468 "assistant",
4469 "assistant2",
4470 "auto_update",
4471 "branches",
4472 "buffer_search",
4473 "channel_modal",
4474 "cli",
4475 "client",
4476 "collab",
4477 "collab_panel",
4478 "command_palette",
4479 "console",
4480 "context_server",
4481 "copilot",
4482 "debug_panel",
4483 "debugger",
4484 "dev",
4485 "diagnostics",
4486 "edit_prediction",
4487 "editor",
4488 "feedback",
4489 "file_finder",
4490 "git",
4491 "git_onboarding",
4492 "git_panel",
4493 "go_to_line",
4494 "icon_theme_selector",
4495 "journal",
4496 "keymap_editor",
4497 "keystroke_input",
4498 "language_selector",
4499 "line_ending",
4500 "lsp_tool",
4501 "markdown",
4502 "menu",
4503 "notebook",
4504 "notification_panel",
4505 "onboarding",
4506 "outline",
4507 "outline_panel",
4508 "pane",
4509 "panel",
4510 "picker",
4511 "project_panel",
4512 "project_search",
4513 "project_symbols",
4514 "projects",
4515 "repl",
4516 "rules_library",
4517 "search",
4518 "settings_profile_selector",
4519 "snippets",
4520 "stash_picker",
4521 "supermaven",
4522 "svg",
4523 "syntax_tree_view",
4524 "tab_switcher",
4525 "task",
4526 "terminal",
4527 "terminal_panel",
4528 "theme_selector",
4529 "toast",
4530 "toolchain",
4531 "variable_list",
4532 "vim",
4533 "window",
4534 "workspace",
4535 "zed",
4536 "zed_predict_onboarding",
4537 "zeta",
4538 ];
4539 assert_eq!(
4540 all_namespaces,
4541 expected_namespaces
4542 .into_iter()
4543 .map(|namespace| namespace.to_string())
4544 .sorted()
4545 .collect::<Vec<_>>()
4546 );
4547 });
4548 }
4549
4550 #[gpui::test]
4551 fn test_bundled_settings_and_themes(cx: &mut App) {
4552 cx.text_system()
4553 .add_fonts(vec![
4554 Assets
4555 .load("fonts/lilex/Lilex-Regular.ttf")
4556 .unwrap()
4557 .unwrap(),
4558 Assets
4559 .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf")
4560 .unwrap()
4561 .unwrap(),
4562 ])
4563 .unwrap();
4564 let themes = ThemeRegistry::default();
4565 settings::init(cx);
4566 theme::init(theme::LoadThemes::JustBase, cx);
4567
4568 let mut has_default_theme = false;
4569 for theme_name in themes.list().into_iter().map(|meta| meta.name) {
4570 let theme = themes.get(&theme_name).unwrap();
4571 assert_eq!(theme.name, theme_name);
4572 if theme.name == ThemeSettings::get(None, cx).active_theme.name {
4573 has_default_theme = true;
4574 }
4575 }
4576 assert!(has_default_theme);
4577 }
4578
4579 #[gpui::test]
4580 async fn test_bundled_files_editor(cx: &mut TestAppContext) {
4581 let app_state = init_test(cx);
4582 cx.update(init);
4583
4584 let project = Project::test(app_state.fs.clone(), [], cx).await;
4585 let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
4586
4587 cx.update(|cx| {
4588 cx.dispatch_action(&OpenDefaultSettings);
4589 });
4590 cx.run_until_parked();
4591
4592 assert_eq!(cx.read(|cx| cx.windows().len()), 1);
4593
4594 let workspace = cx.windows()[0].downcast::<Workspace>().unwrap();
4595 let active_editor = workspace
4596 .update(cx, |workspace, _, cx| {
4597 workspace.active_item_as::<Editor>(cx)
4598 })
4599 .unwrap();
4600 assert!(
4601 active_editor.is_some(),
4602 "Settings action should have opened an editor with the default file contents"
4603 );
4604
4605 let active_editor = active_editor.unwrap();
4606 assert!(
4607 active_editor.read_with(cx, |editor, cx| editor.read_only(cx)),
4608 "Default settings should be readonly"
4609 );
4610 assert!(
4611 active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()),
4612 "The underlying buffer should also be readonly for the shipped default settings"
4613 );
4614 }
4615
4616 #[gpui::test]
4617 async fn test_bundled_languages(cx: &mut TestAppContext) {
4618 let fs = fs::FakeFs::new(cx.background_executor.clone());
4619 env_logger::builder().is_test(true).try_init().ok();
4620 let settings = cx.update(SettingsStore::test);
4621 cx.set_global(settings);
4622 let languages = LanguageRegistry::test(cx.executor());
4623 let languages = Arc::new(languages);
4624 let node_runtime = node_runtime::NodeRuntime::unavailable();
4625 cx.update(|cx| {
4626 languages::init(languages.clone(), fs, node_runtime, cx);
4627 });
4628 for name in languages.language_names() {
4629 languages
4630 .language_for_name(name.as_ref())
4631 .await
4632 .with_context(|| format!("language name {name}"))
4633 .unwrap();
4634 }
4635 cx.run_until_parked();
4636 }
4637
4638 pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
4639 init_test_with_state(cx, cx.update(AppState::test))
4640 }
4641
4642 fn init_test_with_state(
4643 cx: &mut TestAppContext,
4644 mut app_state: Arc<AppState>,
4645 ) -> Arc<AppState> {
4646 cx.update(move |cx| {
4647 env_logger::builder().is_test(true).try_init().ok();
4648
4649 let state = Arc::get_mut(&mut app_state).unwrap();
4650 state.build_window_options = build_window_options;
4651
4652 app_state.languages.add(markdown_language());
4653
4654 gpui_tokio::init(cx);
4655 vim_mode_setting::init(cx);
4656 theme::init(theme::LoadThemes::JustBase, cx);
4657 audio::init(cx);
4658 channel::init(&app_state.client, app_state.user_store.clone(), cx);
4659 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4660 notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
4661 workspace::init(app_state.clone(), cx);
4662 Project::init_settings(cx);
4663 release_channel::init(SemanticVersion::default(), cx);
4664 command_palette::init(cx);
4665 language::init(cx);
4666 editor::init(cx);
4667 collab_ui::init(&app_state, cx);
4668 git_ui::init(cx);
4669 project_panel::init(cx);
4670 outline_panel::init(cx);
4671 terminal_view::init(cx);
4672 copilot::copilot_chat::init(
4673 app_state.fs.clone(),
4674 app_state.client.http_client(),
4675 copilot::copilot_chat::CopilotChatConfiguration::default(),
4676 cx,
4677 );
4678 image_viewer::init(cx);
4679 language_model::init(app_state.client.clone(), cx);
4680 language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
4681 web_search::init(cx);
4682 web_search_providers::init(app_state.client.clone(), cx);
4683 let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
4684 agent_ui::init(
4685 app_state.fs.clone(),
4686 app_state.client.clone(),
4687 prompt_builder.clone(),
4688 app_state.languages.clone(),
4689 false,
4690 cx,
4691 );
4692 repl::init(app_state.fs.clone(), cx);
4693 repl::notebook::init(cx);
4694 tasks_ui::init(cx);
4695 project::debugger::breakpoint_store::BreakpointStore::init(
4696 &app_state.client.clone().into(),
4697 );
4698 project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
4699 debugger_ui::init(cx);
4700 initialize_workspace(app_state.clone(), prompt_builder, cx);
4701 search::init(cx);
4702 app_state
4703 })
4704 }
4705
4706 fn rust_lang() -> Arc<language::Language> {
4707 Arc::new(language::Language::new(
4708 language::LanguageConfig {
4709 name: "Rust".into(),
4710 matcher: LanguageMatcher {
4711 path_suffixes: vec!["rs".to_string()],
4712 ..Default::default()
4713 },
4714 ..Default::default()
4715 },
4716 Some(tree_sitter_rust::LANGUAGE.into()),
4717 ))
4718 }
4719
4720 fn markdown_language() -> Arc<language::Language> {
4721 Arc::new(language::Language::new(
4722 language::LanguageConfig {
4723 name: "Markdown".into(),
4724 matcher: LanguageMatcher {
4725 path_suffixes: vec!["md".to_string()],
4726 ..Default::default()
4727 },
4728 ..Default::default()
4729 },
4730 Some(tree_sitter_md::LANGUAGE.into()),
4731 ))
4732 }
4733
4734 #[track_caller]
4735 fn assert_key_bindings_for(
4736 window: AnyWindowHandle,
4737 cx: &TestAppContext,
4738 actions: Vec<(&'static str, &dyn Action)>,
4739 line: u32,
4740 ) {
4741 let available_actions = cx
4742 .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx)))
4743 .unwrap();
4744 for (key, action) in actions {
4745 let bindings = cx
4746 .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action)))
4747 .unwrap();
4748 // assert that...
4749 assert!(
4750 available_actions.iter().any(|bound_action| {
4751 // actions match...
4752 bound_action.partial_eq(action)
4753 }),
4754 "On {} Failed to find {}",
4755 line,
4756 action.name(),
4757 );
4758 assert!(
4759 // and key strokes contain the given key
4760 bindings
4761 .into_iter()
4762 .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)),
4763 "On {} Failed to find {} with key binding {}",
4764 line,
4765 action.name(),
4766 key
4767 );
4768 }
4769 }
4770
4771 #[gpui::test]
4772 async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) {
4773 // Use the proper initialization for runtime state
4774 let app_state = init_keymap_test(cx);
4775
4776 eprintln!("Running test_opening_project_settings_when_excluded");
4777
4778 // 1. Set up a project with some project settings
4779 let settings_init =
4780 r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#;
4781 app_state
4782 .fs
4783 .as_fake()
4784 .insert_tree(
4785 Path::new("/root"),
4786 json!({
4787 ".zed": {
4788 "settings.json": settings_init
4789 }
4790 }),
4791 )
4792 .await;
4793
4794 eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE");
4795
4796 // 2. Create a project with the file system and load it
4797 let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await;
4798
4799 // Save original settings content for comparison
4800 let original_settings = app_state
4801 .fs
4802 .load(Path::new("/root/.zed/settings.json"))
4803 .await
4804 .unwrap();
4805
4806 let original_settings_str = original_settings.clone();
4807
4808 // Verify settings exist on disk and have expected content
4809 eprintln!("Original settings content: {}", original_settings_str);
4810 assert!(
4811 original_settings_str.contains("UNIQUEVALUE"),
4812 "Test setup failed - settings file doesn't contain our marker"
4813 );
4814
4815 // 3. Add .zed to file scan exclusions in user settings
4816 cx.update_global::<SettingsStore, _>(|store, cx| {
4817 store.update_user_settings(cx, |worktree_settings| {
4818 worktree_settings.project.worktree.file_scan_exclusions =
4819 Some(vec![".zed".to_string()]);
4820 });
4821 });
4822
4823 eprintln!("Added .zed to file_scan_exclusions in settings");
4824
4825 // 4. Run tasks to apply settings
4826 cx.background_executor.run_until_parked();
4827
4828 // 5. Critical: Verify .zed is actually excluded from worktree
4829 let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap());
4830
4831 let has_zed_entry =
4832 cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some());
4833
4834 eprintln!(
4835 "Is .zed directory visible in worktree after exclusion: {}",
4836 has_zed_entry
4837 );
4838
4839 // This assertion verifies the test is set up correctly to show the bug
4840 // If .zed is not excluded, the test will fail here
4841 assert!(
4842 !has_zed_entry,
4843 "Test precondition failed: .zed directory should be excluded but was found in worktree"
4844 );
4845
4846 // 6. Create workspace and trigger the actual function that causes the bug
4847 let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
4848 window
4849 .update(cx, |workspace, window, cx| {
4850 // Call the exact function that contains the bug
4851 eprintln!("About to call open_project_settings_file");
4852 open_project_settings_file(workspace, &OpenProjectSettings, window, cx);
4853 })
4854 .unwrap();
4855
4856 // 7. Run background tasks until completion
4857 cx.background_executor.run_until_parked();
4858
4859 // 8. Verify file contents after calling function
4860 let new_content = app_state
4861 .fs
4862 .load(Path::new("/root/.zed/settings.json"))
4863 .await
4864 .unwrap();
4865
4866 let new_content_str = new_content;
4867 eprintln!("New settings content: {}", new_content_str);
4868
4869 // The bug causes the settings to be overwritten with empty settings
4870 // So if the unique value is no longer present, the bug has been reproduced
4871 let bug_exists = !new_content_str.contains("UNIQUEVALUE");
4872 eprintln!("Bug reproduced: {}", bug_exists);
4873
4874 // This assertion should fail if the bug exists - showing the bug is real
4875 assert!(
4876 new_content_str.contains("UNIQUEVALUE"),
4877 "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost"
4878 );
4879 }
4880}