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