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