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