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