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