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