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