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