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