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