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