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