1pub mod languages;
2pub mod menus;
3#[cfg(any(test, feature = "test-support"))]
4pub mod test;
5use anyhow::Context;
6use assets::Assets;
7use breadcrumbs::Breadcrumbs;
8pub use client;
9use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
10use collections::VecDeque;
11pub use editor;
12use editor::{Editor, MultiBuffer};
13
14use anyhow::anyhow;
15use feedback::{
16 feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
17};
18use futures::StreamExt;
19use gpui::{
20 actions,
21 geometry::vector::vec2f,
22 impl_actions,
23 platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
24 AppContext, ViewContext,
25};
26pub use lsp;
27pub use project;
28use project_panel::ProjectPanel;
29use search::{BufferSearchBar, ProjectSearchBar};
30use serde::Deserialize;
31use serde_json::to_string_pretty;
32use settings::{Settings, DEFAULT_SETTINGS_ASSET_PATH};
33use std::{borrow::Cow, str, sync::Arc};
34use terminal_view::terminal_button::TerminalButton;
35use util::{channel::ReleaseChannel, paths, ResultExt};
36use uuid::Uuid;
37pub use workspace;
38use workspace::{
39 create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow,
40 Workspace,
41};
42
43#[derive(Deserialize, Clone, PartialEq)]
44pub struct OpenBrowser {
45 url: Arc<str>,
46}
47
48impl_actions!(zed, [OpenBrowser]);
49
50actions!(
51 zed,
52 [
53 About,
54 Hide,
55 HideOthers,
56 ShowAll,
57 Minimize,
58 Zoom,
59 ToggleFullScreen,
60 Quit,
61 DebugElements,
62 OpenLog,
63 OpenLicenses,
64 OpenTelemetryLog,
65 OpenKeymap,
66 OpenDefaultSettings,
67 OpenDefaultKeymap,
68 IncreaseBufferFontSize,
69 DecreaseBufferFontSize,
70 ResetBufferFontSize,
71 ResetDatabase,
72 ]
73);
74
75const MIN_FONT_SIZE: f32 = 6.0;
76
77pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
78 cx.add_action(about);
79 cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| {
80 cx.platform().hide();
81 });
82 cx.add_global_action(|_: &HideOthers, cx: &mut gpui::AppContext| {
83 cx.platform().hide_other_apps();
84 });
85 cx.add_global_action(|_: &ShowAll, cx: &mut gpui::AppContext| {
86 cx.platform().unhide_other_apps();
87 });
88 cx.add_action(
89 |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext<Workspace>| {
90 cx.minimize_window();
91 },
92 );
93 cx.add_action(
94 |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext<Workspace>| {
95 cx.zoom_window();
96 },
97 );
98 cx.add_action(
99 |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext<Workspace>| {
100 cx.toggle_full_screen();
101 },
102 );
103 cx.add_action(
104 |workspace: &mut Workspace, _: &ToggleContactsMenu, cx: &mut ViewContext<Workspace>| {
105 if let Some(item) = workspace
106 .titlebar_item()
107 .and_then(|item| item.downcast::<CollabTitlebarItem>())
108 {
109 cx.defer(move |_, cx| {
110 item.update(cx, |item, cx| {
111 item.toggle_contacts_popover(&Default::default(), cx);
112 });
113 });
114 }
115 },
116 );
117 cx.add_global_action(quit);
118 cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
119 cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
120 cx.update_global::<Settings, _, _>(|settings, cx| {
121 settings.buffer_font_size = (settings.buffer_font_size + 1.0).max(MIN_FONT_SIZE);
122 if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
123 *terminal_font_size = (*terminal_font_size + 1.0).max(MIN_FONT_SIZE);
124 }
125 cx.refresh_windows();
126 });
127 });
128 cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
129 cx.update_global::<Settings, _, _>(|settings, cx| {
130 settings.buffer_font_size = (settings.buffer_font_size - 1.0).max(MIN_FONT_SIZE);
131 if let Some(terminal_font_size) = settings.terminal_overrides.font_size.as_mut() {
132 *terminal_font_size = (*terminal_font_size - 1.0).max(MIN_FONT_SIZE);
133 }
134 cx.refresh_windows();
135 });
136 });
137 cx.add_global_action(move |_: &ResetBufferFontSize, cx| {
138 cx.update_global::<Settings, _, _>(|settings, cx| {
139 settings.buffer_font_size = settings.default_buffer_font_size;
140 settings.terminal_overrides.font_size = settings.terminal_defaults.font_size;
141 cx.refresh_windows();
142 });
143 });
144 cx.add_global_action(move |_: &install_cli::Install, cx| {
145 cx.spawn(|cx| async move {
146 install_cli::install_cli(&cx)
147 .await
148 .context("error creating CLI symlink")
149 })
150 .detach_and_log_err(cx);
151 });
152 cx.add_action(
153 move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
154 open_log_file(workspace, cx);
155 },
156 );
157 cx.add_action(
158 move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
159 open_bundled_file(
160 workspace,
161 "licenses.md",
162 "Open Source License Attribution",
163 "Markdown",
164 cx,
165 );
166 },
167 );
168 cx.add_action(
169 move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
170 open_telemetry_log_file(workspace, cx);
171 },
172 );
173 cx.add_action(
174 move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
175 create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx);
176 },
177 );
178 cx.add_action(
179 move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
180 open_bundled_file(
181 workspace,
182 "keymaps/default.json",
183 "Default Key Bindings",
184 "JSON",
185 cx,
186 );
187 },
188 );
189 cx.add_action(
190 move |workspace: &mut Workspace,
191 _: &OpenDefaultSettings,
192 cx: &mut ViewContext<Workspace>| {
193 open_bundled_file(
194 workspace,
195 DEFAULT_SETTINGS_ASSET_PATH,
196 "Default Settings",
197 "JSON",
198 cx,
199 );
200 },
201 );
202 cx.add_action({
203 move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
204 let app_state = workspace.app_state().clone();
205 let markdown = app_state.languages.language_for_name("JSON");
206 let window_id = cx.window_id();
207 cx.spawn(|workspace, mut cx| async move {
208 let markdown = markdown.await.log_err();
209 let content = to_string_pretty(
210 &cx.debug_elements(window_id)
211 .ok_or_else(|| anyhow!("could not debug elements for {window_id}"))?,
212 )
213 .unwrap();
214 workspace
215 .update(&mut cx, |workspace, cx| {
216 workspace.with_local_workspace(cx, move |workspace, cx| {
217 let project = workspace.project().clone();
218
219 let buffer = project
220 .update(cx, |project, cx| {
221 project.create_buffer(&content, markdown, cx)
222 })
223 .expect("creating buffers on a local workspace always succeeds");
224 let buffer = cx.add_model(|cx| {
225 MultiBuffer::singleton(buffer, cx)
226 .with_title("Debug Elements".into())
227 });
228 workspace.add_item(
229 Box::new(cx.add_view(|cx| {
230 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
231 })),
232 cx,
233 );
234 })
235 })?
236 .await
237 })
238 .detach_and_log_err(cx);
239 }
240 });
241 cx.add_action(
242 |workspace: &mut Workspace,
243 _: &project_panel::ToggleFocus,
244 cx: &mut ViewContext<Workspace>| {
245 workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
246 },
247 );
248 cx.add_global_action({
249 let app_state = Arc::downgrade(&app_state);
250 move |_: &NewWindow, cx: &mut AppContext| {
251 if let Some(app_state) = app_state.upgrade() {
252 open_new(&app_state, cx, |workspace, cx| {
253 Editor::new_file(workspace, &Default::default(), cx)
254 })
255 .detach();
256 }
257 }
258 });
259 cx.add_global_action({
260 let app_state = Arc::downgrade(&app_state);
261 move |_: &NewFile, cx: &mut AppContext| {
262 if let Some(app_state) = app_state.upgrade() {
263 open_new(&app_state, cx, |workspace, cx| {
264 Editor::new_file(workspace, &Default::default(), cx)
265 })
266 .detach();
267 }
268 }
269 });
270 activity_indicator::init(cx);
271 lsp_log::init(cx);
272 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
273 settings::KeymapFileContent::load_defaults(cx);
274}
275
276pub fn initialize_workspace(
277 workspace: &mut Workspace,
278 app_state: &Arc<AppState>,
279 cx: &mut ViewContext<Workspace>,
280) {
281 let workspace_handle = cx.handle();
282 cx.subscribe(&workspace_handle, {
283 move |workspace, _, event, cx| {
284 if let workspace::Event::PaneAdded(pane) = event {
285 pane.update(cx, |pane, cx| {
286 pane.toolbar().update(cx, |toolbar, cx| {
287 let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
288 toolbar.add_item(breadcrumbs, cx);
289 let buffer_search_bar = cx.add_view(BufferSearchBar::new);
290 toolbar.add_item(buffer_search_bar, cx);
291 let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
292 toolbar.add_item(project_search_bar, cx);
293 let submit_feedback_button = cx.add_view(|_| SubmitFeedbackButton::new());
294 toolbar.add_item(submit_feedback_button, cx);
295 let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
296 toolbar.add_item(feedback_info_text, cx);
297 let lsp_log_item = cx.add_view(|_| {
298 lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
299 });
300 toolbar.add_item(lsp_log_item, cx);
301 })
302 });
303 }
304 }
305 })
306 .detach();
307
308 cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
309 cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
310
311 let collab_titlebar_item =
312 cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
313 workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
314
315 let project_panel = ProjectPanel::new(workspace, cx);
316 workspace.left_sidebar().update(cx, |sidebar, cx| {
317 sidebar.add_item(
318 "icons/folder_tree_16.svg",
319 "Project Panel".to_string(),
320 project_panel,
321 cx,
322 )
323 });
324
325 let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
326 let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
327 let diagnostic_summary =
328 cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
329 let activity_indicator =
330 activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
331 let active_buffer_language =
332 cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
333 let feedback_button =
334 cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
335 let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
336 workspace.status_bar().update(cx, |status_bar, cx| {
337 status_bar.add_left_item(diagnostic_summary, cx);
338 status_bar.add_left_item(activity_indicator, cx);
339 status_bar.add_right_item(toggle_terminal, cx);
340 status_bar.add_right_item(feedback_button, cx);
341 status_bar.add_right_item(copilot, cx);
342 status_bar.add_right_item(active_buffer_language, cx);
343 status_bar.add_right_item(cursor_position, cx);
344 });
345
346 auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
347
348 vim::observe_keystrokes(cx);
349
350 cx.on_window_should_close(|workspace, cx| {
351 if let Some(task) = workspace.close(&Default::default(), cx) {
352 task.detach_and_log_err(cx);
353 }
354 false
355 });
356}
357
358pub fn build_window_options(
359 bounds: Option<WindowBounds>,
360 display: Option<Uuid>,
361 platform: &dyn Platform,
362) -> WindowOptions<'static> {
363 let bounds = bounds.unwrap_or(WindowBounds::Maximized);
364 let screen = display.and_then(|display| platform.screen_by_id(display));
365
366 WindowOptions {
367 titlebar: Some(TitlebarOptions {
368 title: None,
369 appears_transparent: true,
370 traffic_light_position: Some(vec2f(8., 8.)),
371 }),
372 center: false,
373 focus: true,
374 kind: WindowKind::Normal,
375 is_movable: true,
376 bounds,
377 screen,
378 }
379}
380
381fn quit(_: &Quit, cx: &mut gpui::AppContext) {
382 let should_confirm = cx.global::<Settings>().confirm_quit;
383 cx.spawn(|mut cx| async move {
384 let mut workspaces = cx
385 .window_ids()
386 .into_iter()
387 .filter_map(|window_id| {
388 Some(
389 cx.root_view(window_id)?
390 .clone()
391 .downcast::<Workspace>()?
392 .downgrade(),
393 )
394 })
395 .collect::<Vec<_>>();
396
397 // If multiple windows have unsaved changes, and need a save prompt,
398 // prompt in the active window before switching to a different window.
399 workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
400
401 if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
402 let answer = cx.prompt(
403 workspace.window_id(),
404 PromptLevel::Info,
405 "Are you sure you want to quit?",
406 &["Quit", "Cancel"],
407 );
408
409 if let Some(mut answer) = answer {
410 let answer = answer.next().await;
411 if answer != Some(0) {
412 return Ok(());
413 }
414 }
415 }
416
417 // If the user cancels any save prompt, then keep the app open.
418 for workspace in workspaces {
419 if !workspace
420 .update(&mut cx, |workspace, cx| {
421 workspace.prepare_to_close(true, cx)
422 })?
423 .await?
424 {
425 return Ok(());
426 }
427 }
428 cx.platform().quit();
429 anyhow::Ok(())
430 })
431 .detach_and_log_err(cx);
432}
433
434fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
435 let app_name = cx.global::<ReleaseChannel>().display_name();
436 let version = env!("CARGO_PKG_VERSION");
437 cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
438}
439
440fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
441 const MAX_LINES: usize = 1000;
442
443 workspace
444 .with_local_workspace(cx, move |workspace, cx| {
445 let fs = workspace.app_state().fs.clone();
446 cx.spawn(|workspace, mut cx| async move {
447 let (old_log, new_log) =
448 futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
449
450 let mut lines = VecDeque::with_capacity(MAX_LINES);
451 for line in old_log
452 .iter()
453 .flat_map(|log| log.lines())
454 .chain(new_log.iter().flat_map(|log| log.lines()))
455 {
456 if lines.len() == MAX_LINES {
457 lines.pop_front();
458 }
459 lines.push_back(line);
460 }
461 let log = lines
462 .into_iter()
463 .flat_map(|line| [line, "\n"])
464 .collect::<String>();
465
466 workspace
467 .update(&mut cx, |workspace, cx| {
468 let project = workspace.project().clone();
469 let buffer = project
470 .update(cx, |project, cx| project.create_buffer("", None, cx))
471 .expect("creating buffers on a local workspace always succeeds");
472 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
473
474 let buffer = cx.add_model(|cx| {
475 MultiBuffer::singleton(buffer, cx).with_title("Log".into())
476 });
477 workspace.add_item(
478 Box::new(
479 cx.add_view(|cx| {
480 Editor::for_multibuffer(buffer, Some(project), cx)
481 }),
482 ),
483 cx,
484 );
485 })
486 .log_err();
487 })
488 .detach();
489 })
490 .detach();
491}
492
493fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
494 workspace.with_local_workspace(cx, move |workspace, cx| {
495 let app_state = workspace.app_state().clone();
496 cx.spawn(|workspace, mut cx| async move {
497 async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
498 let path = app_state.client.telemetry().log_file_path()?;
499 app_state.fs.load(&path).await.log_err()
500 }
501
502 let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
503
504 const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
505 let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
506 if let Some(newline_offset) = log[start_offset..].find('\n') {
507 start_offset += newline_offset + 1;
508 }
509 let log_suffix = &log[start_offset..];
510 let json = app_state.languages.language_for_name("JSON").await.log_err();
511
512 workspace.update(&mut cx, |workspace, cx| {
513 let project = workspace.project().clone();
514 let buffer = project
515 .update(cx, |project, cx| project.create_buffer("", None, cx))
516 .expect("creating buffers on a local workspace always succeeds");
517 buffer.update(cx, |buffer, cx| {
518 buffer.set_language(json, cx);
519 buffer.edit(
520 [(
521 0..0,
522 concat!(
523 "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
524 "// Telemetry can be disabled via the `settings.json` file.\n",
525 "// Here is the data that has been reported for the current session:\n",
526 "\n"
527 ),
528 )],
529 None,
530 cx,
531 );
532 buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
533 });
534
535 let buffer = cx.add_model(|cx| {
536 MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
537 });
538 workspace.add_item(
539 Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
540 cx,
541 );
542 }).log_err()?;
543
544 Some(())
545 })
546 .detach();
547 }).detach();
548}
549
550fn open_bundled_file(
551 workspace: &mut Workspace,
552 asset_path: &'static str,
553 title: &'static str,
554 language: &'static str,
555 cx: &mut ViewContext<Workspace>,
556) {
557 let language = workspace.app_state().languages.language_for_name(language);
558 cx.spawn(|workspace, mut cx| async move {
559 let language = language.await.log_err();
560 workspace
561 .update(&mut cx, |workspace, cx| {
562 workspace.with_local_workspace(cx, |workspace, cx| {
563 let project = workspace.project();
564 let buffer = project.update(cx, |project, cx| {
565 let text = Assets::get(asset_path)
566 .map(|f| f.data)
567 .unwrap_or_else(|| Cow::Borrowed(b"File not found"));
568 let text = str::from_utf8(text.as_ref()).unwrap();
569 project
570 .create_buffer(text, language, cx)
571 .expect("creating buffers on a local workspace always succeeds")
572 });
573 let buffer = cx.add_model(|cx| {
574 MultiBuffer::singleton(buffer, cx).with_title(title.into())
575 });
576 workspace.add_item(
577 Box::new(cx.add_view(|cx| {
578 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
579 })),
580 cx,
581 );
582 })
583 })?
584 .await
585 })
586 .detach_and_log_err(cx);
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use assets::Assets;
593 use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
594 use gpui::{executor::Deterministic, AppContext, AssetSource, TestAppContext, ViewHandle};
595 use language::LanguageRegistry;
596 use node_runtime::NodeRuntime;
597 use project::{Project, ProjectPath};
598 use serde_json::json;
599 use std::{
600 collections::HashSet,
601 path::{Path, PathBuf},
602 };
603 use theme::ThemeRegistry;
604 use util::http::FakeHttpClient;
605 use workspace::{
606 item::{Item, ItemHandle},
607 open_new, open_paths, pane, NewFile, Pane, SplitDirection, WorkspaceHandle,
608 };
609
610 #[gpui::test]
611 async fn test_open_paths_action(cx: &mut TestAppContext) {
612 let app_state = init(cx);
613 app_state
614 .fs
615 .as_fake()
616 .insert_tree(
617 "/root",
618 json!({
619 "a": {
620 "aa": null,
621 "ab": null,
622 },
623 "b": {
624 "ba": null,
625 "bb": null,
626 },
627 "c": {
628 "ca": null,
629 "cb": null,
630 },
631 "d": {
632 "da": null,
633 "db": null,
634 },
635 }),
636 )
637 .await;
638
639 cx.update(|cx| {
640 open_paths(
641 &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
642 &app_state,
643 None,
644 cx,
645 )
646 })
647 .await
648 .unwrap();
649 assert_eq!(cx.window_ids().len(), 1);
650
651 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
652 .await
653 .unwrap();
654 assert_eq!(cx.window_ids().len(), 1);
655 let workspace_1 = cx
656 .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
657 .unwrap()
658 .downcast::<Workspace>()
659 .unwrap();
660 workspace_1.update(cx, |workspace, cx| {
661 assert_eq!(workspace.worktrees(cx).count(), 2);
662 assert!(workspace.left_sidebar().read(cx).is_open());
663 assert!(workspace.active_pane().is_focused(cx));
664 });
665
666 cx.update(|cx| {
667 open_paths(
668 &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
669 &app_state,
670 None,
671 cx,
672 )
673 })
674 .await
675 .unwrap();
676 assert_eq!(cx.window_ids().len(), 2);
677
678 // Replace existing windows
679 let window_id = cx.window_ids()[0];
680 cx.update(|cx| {
681 open_paths(
682 &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
683 &app_state,
684 Some(window_id),
685 cx,
686 )
687 })
688 .await
689 .unwrap();
690 assert_eq!(cx.window_ids().len(), 2);
691 let workspace_1 = cx
692 .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
693 .unwrap()
694 .clone()
695 .downcast::<Workspace>()
696 .unwrap();
697 workspace_1.update(cx, |workspace, cx| {
698 assert_eq!(
699 workspace
700 .worktrees(cx)
701 .map(|w| w.read(cx).abs_path())
702 .collect::<Vec<_>>(),
703 &[Path::new("/root/c").into(), Path::new("/root/d").into()]
704 );
705 assert!(workspace.left_sidebar().read(cx).is_open());
706 assert!(workspace.active_pane().is_focused(cx));
707 });
708 }
709
710 #[gpui::test]
711 async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
712 let app_state = init(cx);
713 app_state
714 .fs
715 .as_fake()
716 .insert_tree("/root", json!({"a": "hey"}))
717 .await;
718
719 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
720 .await
721 .unwrap();
722 assert_eq!(cx.window_ids().len(), 1);
723
724 // When opening the workspace, the window is not in a edited state.
725 let workspace = cx
726 .read_window(cx.window_ids()[0], |cx| cx.root_view().clone())
727 .unwrap()
728 .downcast::<Workspace>()
729 .unwrap();
730 let editor = workspace.read_with(cx, |workspace, cx| {
731 workspace
732 .active_item(cx)
733 .unwrap()
734 .downcast::<Editor>()
735 .unwrap()
736 });
737 assert!(!cx.is_window_edited(workspace.window_id()));
738
739 // Editing a buffer marks the window as edited.
740 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
741 assert!(cx.is_window_edited(workspace.window_id()));
742
743 // Undoing the edit restores the window's edited state.
744 editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
745 assert!(!cx.is_window_edited(workspace.window_id()));
746
747 // Redoing the edit marks the window as edited again.
748 editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
749 assert!(cx.is_window_edited(workspace.window_id()));
750
751 // Closing the item restores the window's edited state.
752 let close = workspace.update(cx, |workspace, cx| {
753 drop(editor);
754 Pane::close_active_item(workspace, &Default::default(), cx).unwrap()
755 });
756 executor.run_until_parked();
757 cx.simulate_prompt_answer(workspace.window_id(), 1);
758 close.await.unwrap();
759 assert!(!cx.is_window_edited(workspace.window_id()));
760
761 // Opening the buffer again doesn't impact the window's edited state.
762 cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
763 .await
764 .unwrap();
765 let editor = workspace.read_with(cx, |workspace, cx| {
766 workspace
767 .active_item(cx)
768 .unwrap()
769 .downcast::<Editor>()
770 .unwrap()
771 });
772 assert!(!cx.is_window_edited(workspace.window_id()));
773
774 // Editing the buffer marks the window as edited.
775 editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
776 assert!(cx.is_window_edited(workspace.window_id()));
777
778 // Ensure closing the window via the mouse gets preempted due to the
779 // buffer having unsaved changes.
780 assert!(!cx.simulate_window_close(workspace.window_id()));
781 executor.run_until_parked();
782 assert_eq!(cx.window_ids().len(), 1);
783
784 // The window is successfully closed after the user dismisses the prompt.
785 cx.simulate_prompt_answer(workspace.window_id(), 1);
786 executor.run_until_parked();
787 assert_eq!(cx.window_ids().len(), 0);
788 }
789
790 #[gpui::test]
791 async fn test_new_empty_workspace(cx: &mut TestAppContext) {
792 let app_state = init(cx);
793 cx.update(|cx| {
794 open_new(&app_state, cx, |workspace, cx| {
795 Editor::new_file(workspace, &Default::default(), cx)
796 })
797 })
798 .await;
799
800 let window_id = *cx.window_ids().first().unwrap();
801 let workspace = cx
802 .read_window(window_id, |cx| cx.root_view().clone())
803 .unwrap()
804 .downcast::<Workspace>()
805 .unwrap();
806
807 let editor = workspace.update(cx, |workspace, cx| {
808 workspace
809 .active_item(cx)
810 .unwrap()
811 .downcast::<editor::Editor>()
812 .unwrap()
813 });
814
815 editor.update(cx, |editor, cx| {
816 assert!(editor.text(cx).is_empty());
817 });
818
819 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
820 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
821 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
822 save_task.await.unwrap();
823 editor.read_with(cx, |editor, cx| {
824 assert!(!editor.is_dirty(cx));
825 assert_eq!(editor.title(cx), "the-new-name");
826 });
827 }
828
829 #[gpui::test]
830 async fn test_open_entry(cx: &mut TestAppContext) {
831 let app_state = init(cx);
832 app_state
833 .fs
834 .as_fake()
835 .insert_tree(
836 "/root",
837 json!({
838 "a": {
839 "file1": "contents 1",
840 "file2": "contents 2",
841 "file3": "contents 3",
842 },
843 }),
844 )
845 .await;
846
847 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
848 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
849
850 let entries = cx.read(|cx| workspace.file_project_paths(cx));
851 let file1 = entries[0].clone();
852 let file2 = entries[1].clone();
853 let file3 = entries[2].clone();
854
855 // Open the first entry
856 let entry_1 = workspace
857 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
858 .await
859 .unwrap();
860 cx.read(|cx| {
861 let pane = workspace.read(cx).active_pane().read(cx);
862 assert_eq!(
863 pane.active_item().unwrap().project_path(cx),
864 Some(file1.clone())
865 );
866 assert_eq!(pane.items_len(), 1);
867 });
868
869 // Open the second entry
870 workspace
871 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
872 .await
873 .unwrap();
874 cx.read(|cx| {
875 let pane = workspace.read(cx).active_pane().read(cx);
876 assert_eq!(
877 pane.active_item().unwrap().project_path(cx),
878 Some(file2.clone())
879 );
880 assert_eq!(pane.items_len(), 2);
881 });
882
883 // Open the first entry again. The existing pane item is activated.
884 let entry_1b = workspace
885 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
886 .await
887 .unwrap();
888 assert_eq!(entry_1.id(), entry_1b.id());
889
890 cx.read(|cx| {
891 let pane = workspace.read(cx).active_pane().read(cx);
892 assert_eq!(
893 pane.active_item().unwrap().project_path(cx),
894 Some(file1.clone())
895 );
896 assert_eq!(pane.items_len(), 2);
897 });
898
899 // Split the pane with the first entry, then open the second entry again.
900 workspace
901 .update(cx, |w, cx| {
902 w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
903 w.open_path(file2.clone(), None, true, cx)
904 })
905 .await
906 .unwrap();
907
908 workspace.read_with(cx, |w, cx| {
909 assert_eq!(
910 w.active_pane()
911 .read(cx)
912 .active_item()
913 .unwrap()
914 .project_path(cx),
915 Some(file2.clone())
916 );
917 });
918
919 // Open the third entry twice concurrently. Only one pane item is added.
920 let (t1, t2) = workspace.update(cx, |w, cx| {
921 (
922 w.open_path(file3.clone(), None, true, cx),
923 w.open_path(file3.clone(), None, true, cx),
924 )
925 });
926 t1.await.unwrap();
927 t2.await.unwrap();
928 cx.read(|cx| {
929 let pane = workspace.read(cx).active_pane().read(cx);
930 assert_eq!(
931 pane.active_item().unwrap().project_path(cx),
932 Some(file3.clone())
933 );
934 let pane_entries = pane
935 .items()
936 .map(|i| i.project_path(cx).unwrap())
937 .collect::<Vec<_>>();
938 assert_eq!(pane_entries, &[file1, file2, file3]);
939 });
940 }
941
942 #[gpui::test]
943 async fn test_open_paths(cx: &mut TestAppContext) {
944 let app_state = init(cx);
945
946 app_state
947 .fs
948 .as_fake()
949 .insert_tree(
950 "/",
951 json!({
952 "dir1": {
953 "a.txt": ""
954 },
955 "dir2": {
956 "b.txt": ""
957 },
958 "dir3": {
959 "c.txt": ""
960 },
961 "d.txt": ""
962 }),
963 )
964 .await;
965
966 let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
967 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
968
969 // Open a file within an existing worktree.
970 workspace
971 .update(cx, |view, cx| {
972 view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
973 })
974 .await;
975 cx.read(|cx| {
976 assert_eq!(
977 workspace
978 .read(cx)
979 .active_pane()
980 .read(cx)
981 .active_item()
982 .unwrap()
983 .as_any()
984 .downcast_ref::<Editor>()
985 .unwrap()
986 .read(cx)
987 .title(cx),
988 "a.txt"
989 );
990 });
991
992 // Open a file outside of any existing worktree.
993 workspace
994 .update(cx, |view, cx| {
995 view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
996 })
997 .await;
998 cx.read(|cx| {
999 let worktree_roots = workspace
1000 .read(cx)
1001 .worktrees(cx)
1002 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1003 .collect::<HashSet<_>>();
1004 assert_eq!(
1005 worktree_roots,
1006 vec!["/dir1", "/dir2/b.txt"]
1007 .into_iter()
1008 .map(Path::new)
1009 .collect(),
1010 );
1011 assert_eq!(
1012 workspace
1013 .read(cx)
1014 .active_pane()
1015 .read(cx)
1016 .active_item()
1017 .unwrap()
1018 .as_any()
1019 .downcast_ref::<Editor>()
1020 .unwrap()
1021 .read(cx)
1022 .title(cx),
1023 "b.txt"
1024 );
1025 });
1026
1027 // Ensure opening a directory and one of its children only adds one worktree.
1028 workspace
1029 .update(cx, |view, cx| {
1030 view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
1031 })
1032 .await;
1033 cx.read(|cx| {
1034 let worktree_roots = workspace
1035 .read(cx)
1036 .worktrees(cx)
1037 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1038 .collect::<HashSet<_>>();
1039 assert_eq!(
1040 worktree_roots,
1041 vec!["/dir1", "/dir2/b.txt", "/dir3"]
1042 .into_iter()
1043 .map(Path::new)
1044 .collect(),
1045 );
1046 assert_eq!(
1047 workspace
1048 .read(cx)
1049 .active_pane()
1050 .read(cx)
1051 .active_item()
1052 .unwrap()
1053 .as_any()
1054 .downcast_ref::<Editor>()
1055 .unwrap()
1056 .read(cx)
1057 .title(cx),
1058 "c.txt"
1059 );
1060 });
1061
1062 // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
1063 workspace
1064 .update(cx, |view, cx| {
1065 view.open_paths(vec!["/d.txt".into()], false, cx)
1066 })
1067 .await;
1068 cx.read(|cx| {
1069 let worktree_roots = workspace
1070 .read(cx)
1071 .worktrees(cx)
1072 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1073 .collect::<HashSet<_>>();
1074 assert_eq!(
1075 worktree_roots,
1076 vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
1077 .into_iter()
1078 .map(Path::new)
1079 .collect(),
1080 );
1081
1082 let visible_worktree_roots = workspace
1083 .read(cx)
1084 .visible_worktrees(cx)
1085 .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
1086 .collect::<HashSet<_>>();
1087 assert_eq!(
1088 visible_worktree_roots,
1089 vec!["/dir1", "/dir2/b.txt", "/dir3"]
1090 .into_iter()
1091 .map(Path::new)
1092 .collect(),
1093 );
1094
1095 assert_eq!(
1096 workspace
1097 .read(cx)
1098 .active_pane()
1099 .read(cx)
1100 .active_item()
1101 .unwrap()
1102 .as_any()
1103 .downcast_ref::<Editor>()
1104 .unwrap()
1105 .read(cx)
1106 .title(cx),
1107 "d.txt"
1108 );
1109 });
1110 }
1111
1112 #[gpui::test]
1113 async fn test_save_conflicting_item(cx: &mut TestAppContext) {
1114 let app_state = init(cx);
1115 app_state
1116 .fs
1117 .as_fake()
1118 .insert_tree("/root", json!({ "a.txt": "" }))
1119 .await;
1120
1121 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1122 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1123
1124 // Open a file within an existing worktree.
1125 workspace
1126 .update(cx, |view, cx| {
1127 view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
1128 })
1129 .await;
1130 let editor = cx.read(|cx| {
1131 let pane = workspace.read(cx).active_pane().read(cx);
1132 let item = pane.active_item().unwrap();
1133 item.downcast::<Editor>().unwrap()
1134 });
1135
1136 editor.update(cx, |editor, cx| editor.handle_input("x", cx));
1137 app_state
1138 .fs
1139 .as_fake()
1140 .insert_file("/root/a.txt", "changed".to_string())
1141 .await;
1142 editor
1143 .condition(cx, |editor, cx| editor.has_conflict(cx))
1144 .await;
1145 cx.read(|cx| assert!(editor.is_dirty(cx)));
1146
1147 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1148 cx.simulate_prompt_answer(window_id, 0);
1149 save_task.await.unwrap();
1150 editor.read_with(cx, |editor, cx| {
1151 assert!(!editor.is_dirty(cx));
1152 assert!(!editor.has_conflict(cx));
1153 });
1154 }
1155
1156 #[gpui::test]
1157 async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
1158 let app_state = init(cx);
1159 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1160
1161 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1162 project.update(cx, |project, _| project.languages().add(rust_lang()));
1163 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1164 let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
1165
1166 // Create a new untitled buffer
1167 cx.dispatch_action(window_id, NewFile);
1168 let editor = workspace.read_with(cx, |workspace, cx| {
1169 workspace
1170 .active_item(cx)
1171 .unwrap()
1172 .downcast::<Editor>()
1173 .unwrap()
1174 });
1175
1176 editor.update(cx, |editor, cx| {
1177 assert!(!editor.is_dirty(cx));
1178 assert_eq!(editor.title(cx), "untitled");
1179 assert!(Arc::ptr_eq(
1180 &editor.language_at(0, cx).unwrap(),
1181 &languages::PLAIN_TEXT
1182 ));
1183 editor.handle_input("hi", cx);
1184 assert!(editor.is_dirty(cx));
1185 });
1186
1187 // Save the buffer. This prompts for a filename.
1188 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1189 cx.simulate_new_path_selection(|parent_dir| {
1190 assert_eq!(parent_dir, Path::new("/root"));
1191 Some(parent_dir.join("the-new-name.rs"))
1192 });
1193 cx.read(|cx| {
1194 assert!(editor.is_dirty(cx));
1195 assert_eq!(editor.read(cx).title(cx), "untitled");
1196 });
1197
1198 // When the save completes, the buffer's title is updated and the language is assigned based
1199 // on the path.
1200 save_task.await.unwrap();
1201 editor.read_with(cx, |editor, cx| {
1202 assert!(!editor.is_dirty(cx));
1203 assert_eq!(editor.title(cx), "the-new-name.rs");
1204 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
1205 });
1206
1207 // Edit the file and save it again. This time, there is no filename prompt.
1208 editor.update(cx, |editor, cx| {
1209 editor.handle_input(" there", cx);
1210 assert!(editor.is_dirty(cx));
1211 });
1212 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1213 save_task.await.unwrap();
1214 assert!(!cx.did_prompt_for_new_path());
1215 editor.read_with(cx, |editor, cx| {
1216 assert!(!editor.is_dirty(cx));
1217 assert_eq!(editor.title(cx), "the-new-name.rs")
1218 });
1219
1220 // Open the same newly-created file in another pane item. The new editor should reuse
1221 // the same buffer.
1222 cx.dispatch_action(window_id, NewFile);
1223 workspace
1224 .update(cx, |workspace, cx| {
1225 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
1226 workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
1227 })
1228 .await
1229 .unwrap();
1230 let editor2 = workspace.update(cx, |workspace, cx| {
1231 workspace
1232 .active_item(cx)
1233 .unwrap()
1234 .downcast::<Editor>()
1235 .unwrap()
1236 });
1237 cx.read(|cx| {
1238 assert_eq!(
1239 editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
1240 editor.read(cx).buffer().read(cx).as_singleton().unwrap()
1241 );
1242 })
1243 }
1244
1245 #[gpui::test]
1246 async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
1247 let app_state = init(cx);
1248 app_state.fs.create_dir(Path::new("/root")).await.unwrap();
1249
1250 let project = Project::test(app_state.fs.clone(), [], cx).await;
1251 project.update(cx, |project, _| project.languages().add(rust_lang()));
1252 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1253
1254 // Create a new untitled buffer
1255 cx.dispatch_action(window_id, NewFile);
1256 let editor = workspace.read_with(cx, |workspace, cx| {
1257 workspace
1258 .active_item(cx)
1259 .unwrap()
1260 .downcast::<Editor>()
1261 .unwrap()
1262 });
1263
1264 editor.update(cx, |editor, cx| {
1265 assert!(Arc::ptr_eq(
1266 &editor.language_at(0, cx).unwrap(),
1267 &languages::PLAIN_TEXT
1268 ));
1269 editor.handle_input("hi", cx);
1270 assert!(editor.is_dirty(cx));
1271 });
1272
1273 // Save the buffer. This prompts for a filename.
1274 let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
1275 cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
1276 save_task.await.unwrap();
1277 // The buffer is not dirty anymore and the language is assigned based on the path.
1278 editor.read_with(cx, |editor, cx| {
1279 assert!(!editor.is_dirty(cx));
1280 assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
1281 });
1282 }
1283
1284 #[gpui::test]
1285 async fn test_pane_actions(cx: &mut TestAppContext) {
1286 init(cx);
1287
1288 let app_state = cx.update(AppState::test);
1289 app_state
1290 .fs
1291 .as_fake()
1292 .insert_tree(
1293 "/root",
1294 json!({
1295 "a": {
1296 "file1": "contents 1",
1297 "file2": "contents 2",
1298 "file3": "contents 3",
1299 },
1300 }),
1301 )
1302 .await;
1303
1304 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1305 let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1306
1307 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1308 let file1 = entries[0].clone();
1309
1310 let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
1311
1312 workspace
1313 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1314 .await
1315 .unwrap();
1316
1317 let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
1318 let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
1319 assert_eq!(editor.project_path(cx), Some(file1.clone()));
1320 let buffer = editor.update(cx, |editor, cx| {
1321 editor.insert("dirt", cx);
1322 editor.buffer().downgrade()
1323 });
1324 (editor.downgrade(), buffer)
1325 });
1326
1327 cx.dispatch_action(window_id, pane::SplitRight);
1328 let editor_2 = cx.update(|cx| {
1329 let pane_2 = workspace.read(cx).active_pane().clone();
1330 assert_ne!(pane_1, pane_2);
1331
1332 let pane2_item = pane_2.read(cx).active_item().unwrap();
1333 assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
1334
1335 pane2_item.downcast::<Editor>().unwrap().downgrade()
1336 });
1337 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1338
1339 cx.foreground().run_until_parked();
1340 workspace.read_with(cx, |workspace, _| {
1341 assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane
1342 assert_eq!(workspace.active_pane(), &pane_1);
1343 });
1344
1345 cx.dispatch_action(window_id, workspace::CloseActiveItem);
1346 cx.foreground().run_until_parked();
1347 cx.simulate_prompt_answer(window_id, 1);
1348 cx.foreground().run_until_parked();
1349
1350 workspace.read_with(cx, |workspace, cx| {
1351 assert_eq!(workspace.panes().len(), 2);
1352 assert!(workspace.active_item(cx).is_none());
1353 });
1354
1355 cx.assert_dropped(editor_1);
1356 cx.assert_dropped(editor_2);
1357 cx.assert_dropped(buffer);
1358 }
1359
1360 #[gpui::test]
1361 async fn test_navigation(cx: &mut TestAppContext) {
1362 let app_state = init(cx);
1363 app_state
1364 .fs
1365 .as_fake()
1366 .insert_tree(
1367 "/root",
1368 json!({
1369 "a": {
1370 "file1": "contents 1\n".repeat(20),
1371 "file2": "contents 2\n".repeat(20),
1372 "file3": "contents 3\n".repeat(20),
1373 },
1374 }),
1375 )
1376 .await;
1377
1378 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1379 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1380
1381 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1382 let file1 = entries[0].clone();
1383 let file2 = entries[1].clone();
1384 let file3 = entries[2].clone();
1385
1386 let editor1 = workspace
1387 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1388 .await
1389 .unwrap()
1390 .downcast::<Editor>()
1391 .unwrap();
1392 editor1.update(cx, |editor, cx| {
1393 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1394 s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
1395 });
1396 });
1397 let editor2 = workspace
1398 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1399 .await
1400 .unwrap()
1401 .downcast::<Editor>()
1402 .unwrap();
1403 let editor3 = workspace
1404 .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1405 .await
1406 .unwrap()
1407 .downcast::<Editor>()
1408 .unwrap();
1409
1410 editor3
1411 .update(cx, |editor, cx| {
1412 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
1413 s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
1414 });
1415 editor.newline(&Default::default(), cx);
1416 editor.newline(&Default::default(), cx);
1417 editor.move_down(&Default::default(), cx);
1418 editor.move_down(&Default::default(), cx);
1419 editor.save(project.clone(), cx)
1420 })
1421 .await
1422 .unwrap();
1423 editor3.update(cx, |editor, cx| {
1424 editor.set_scroll_position(vec2f(0., 12.5), cx)
1425 });
1426 assert_eq!(
1427 active_location(&workspace, cx),
1428 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1429 );
1430
1431 workspace
1432 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1433 .await
1434 .unwrap();
1435 assert_eq!(
1436 active_location(&workspace, cx),
1437 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1438 );
1439
1440 workspace
1441 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1442 .await
1443 .unwrap();
1444 assert_eq!(
1445 active_location(&workspace, cx),
1446 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1447 );
1448
1449 workspace
1450 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1451 .await
1452 .unwrap();
1453 assert_eq!(
1454 active_location(&workspace, cx),
1455 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1456 );
1457
1458 workspace
1459 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1460 .await
1461 .unwrap();
1462 assert_eq!(
1463 active_location(&workspace, cx),
1464 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1465 );
1466
1467 // Go back one more time and ensure we don't navigate past the first item in the history.
1468 workspace
1469 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1470 .await
1471 .unwrap();
1472 assert_eq!(
1473 active_location(&workspace, cx),
1474 (file1.clone(), DisplayPoint::new(0, 0), 0.)
1475 );
1476
1477 workspace
1478 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1479 .await
1480 .unwrap();
1481 assert_eq!(
1482 active_location(&workspace, cx),
1483 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1484 );
1485
1486 workspace
1487 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1488 .await
1489 .unwrap();
1490 assert_eq!(
1491 active_location(&workspace, cx),
1492 (file2.clone(), DisplayPoint::new(0, 0), 0.)
1493 );
1494
1495 // Go forward to an item that has been closed, ensuring it gets re-opened at the same
1496 // location.
1497 workspace
1498 .update(cx, |workspace, cx| {
1499 let editor3_id = editor3.id();
1500 drop(editor3);
1501 Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor3_id, cx)
1502 })
1503 .await
1504 .unwrap();
1505 workspace
1506 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1507 .await
1508 .unwrap();
1509 assert_eq!(
1510 active_location(&workspace, cx),
1511 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1512 );
1513
1514 workspace
1515 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1516 .await
1517 .unwrap();
1518 assert_eq!(
1519 active_location(&workspace, cx),
1520 (file3.clone(), DisplayPoint::new(16, 0), 12.5)
1521 );
1522
1523 workspace
1524 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1525 .await
1526 .unwrap();
1527 assert_eq!(
1528 active_location(&workspace, cx),
1529 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1530 );
1531
1532 // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
1533 workspace
1534 .update(cx, |workspace, cx| {
1535 let editor2_id = editor2.id();
1536 drop(editor2);
1537 Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor2_id, cx)
1538 })
1539 .await
1540 .unwrap();
1541 app_state
1542 .fs
1543 .remove_file(Path::new("/root/a/file2"), Default::default())
1544 .await
1545 .unwrap();
1546 workspace
1547 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1548 .await
1549 .unwrap();
1550 assert_eq!(
1551 active_location(&workspace, cx),
1552 (file1.clone(), DisplayPoint::new(10, 0), 0.)
1553 );
1554 workspace
1555 .update(cx, |w, cx| Pane::go_forward(w, None, cx))
1556 .await
1557 .unwrap();
1558 assert_eq!(
1559 active_location(&workspace, cx),
1560 (file3.clone(), DisplayPoint::new(0, 0), 0.)
1561 );
1562
1563 // Modify file to collapse multiple nav history entries into the same location.
1564 // Ensure we don't visit the same location twice when navigating.
1565 editor1.update(cx, |editor, cx| {
1566 editor.change_selections(None, cx, |s| {
1567 s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
1568 })
1569 });
1570
1571 for _ in 0..5 {
1572 editor1.update(cx, |editor, cx| {
1573 editor.change_selections(None, cx, |s| {
1574 s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
1575 });
1576 });
1577 editor1.update(cx, |editor, cx| {
1578 editor.change_selections(None, cx, |s| {
1579 s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
1580 })
1581 });
1582 }
1583
1584 editor1.update(cx, |editor, cx| {
1585 editor.transact(cx, |editor, cx| {
1586 editor.change_selections(None, cx, |s| {
1587 s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
1588 });
1589 editor.insert("", cx);
1590 })
1591 });
1592
1593 editor1.update(cx, |editor, cx| {
1594 editor.change_selections(None, cx, |s| {
1595 s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
1596 })
1597 });
1598 workspace
1599 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1600 .await
1601 .unwrap();
1602 assert_eq!(
1603 active_location(&workspace, cx),
1604 (file1.clone(), DisplayPoint::new(2, 0), 0.)
1605 );
1606 workspace
1607 .update(cx, |w, cx| Pane::go_back(w, None, cx))
1608 .await
1609 .unwrap();
1610 assert_eq!(
1611 active_location(&workspace, cx),
1612 (file1.clone(), DisplayPoint::new(3, 0), 0.)
1613 );
1614
1615 fn active_location(
1616 workspace: &ViewHandle<Workspace>,
1617 cx: &mut TestAppContext,
1618 ) -> (ProjectPath, DisplayPoint, f32) {
1619 workspace.update(cx, |workspace, cx| {
1620 let item = workspace.active_item(cx).unwrap();
1621 let editor = item.downcast::<Editor>().unwrap();
1622 let (selections, scroll_position) = editor.update(cx, |editor, cx| {
1623 (
1624 editor.selections.display_ranges(cx),
1625 editor.scroll_position(cx),
1626 )
1627 });
1628 (
1629 item.project_path(cx).unwrap(),
1630 selections[0].start,
1631 scroll_position.y(),
1632 )
1633 })
1634 }
1635 }
1636
1637 #[gpui::test]
1638 async fn test_reopening_closed_items(cx: &mut TestAppContext) {
1639 let app_state = init(cx);
1640 app_state
1641 .fs
1642 .as_fake()
1643 .insert_tree(
1644 "/root",
1645 json!({
1646 "a": {
1647 "file1": "",
1648 "file2": "",
1649 "file3": "",
1650 "file4": "",
1651 },
1652 }),
1653 )
1654 .await;
1655
1656 let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1657 let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1658 let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
1659
1660 let entries = cx.read(|cx| workspace.file_project_paths(cx));
1661 let file1 = entries[0].clone();
1662 let file2 = entries[1].clone();
1663 let file3 = entries[2].clone();
1664 let file4 = entries[3].clone();
1665
1666 let file1_item_id = workspace
1667 .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
1668 .await
1669 .unwrap()
1670 .id();
1671 let file2_item_id = workspace
1672 .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
1673 .await
1674 .unwrap()
1675 .id();
1676 let file3_item_id = workspace
1677 .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
1678 .await
1679 .unwrap()
1680 .id();
1681 let file4_item_id = workspace
1682 .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
1683 .await
1684 .unwrap()
1685 .id();
1686 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1687
1688 // Close all the pane items in some arbitrary order.
1689 workspace
1690 .update(cx, |workspace, cx| {
1691 Pane::close_item_by_id(workspace, pane.clone(), file1_item_id, cx)
1692 })
1693 .await
1694 .unwrap();
1695 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1696
1697 workspace
1698 .update(cx, |workspace, cx| {
1699 Pane::close_item_by_id(workspace, pane.clone(), file4_item_id, cx)
1700 })
1701 .await
1702 .unwrap();
1703 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1704
1705 workspace
1706 .update(cx, |workspace, cx| {
1707 Pane::close_item_by_id(workspace, pane.clone(), file2_item_id, cx)
1708 })
1709 .await
1710 .unwrap();
1711 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1712
1713 workspace
1714 .update(cx, |workspace, cx| {
1715 Pane::close_item_by_id(workspace, pane.clone(), file3_item_id, cx)
1716 })
1717 .await
1718 .unwrap();
1719 assert_eq!(active_path(&workspace, cx), None);
1720
1721 // Reopen all the closed items, ensuring they are reopened in the same order
1722 // in which they were closed.
1723 workspace
1724 .update(cx, Pane::reopen_closed_item)
1725 .await
1726 .unwrap();
1727 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1728
1729 workspace
1730 .update(cx, Pane::reopen_closed_item)
1731 .await
1732 .unwrap();
1733 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1734
1735 workspace
1736 .update(cx, Pane::reopen_closed_item)
1737 .await
1738 .unwrap();
1739 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1740
1741 workspace
1742 .update(cx, Pane::reopen_closed_item)
1743 .await
1744 .unwrap();
1745 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1746
1747 // Reopening past the last closed item is a no-op.
1748 workspace
1749 .update(cx, Pane::reopen_closed_item)
1750 .await
1751 .unwrap();
1752 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1753
1754 // Reopening closed items doesn't interfere with navigation history.
1755 workspace
1756 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1757 .await
1758 .unwrap();
1759 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1760
1761 workspace
1762 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1763 .await
1764 .unwrap();
1765 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1766
1767 workspace
1768 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1769 .await
1770 .unwrap();
1771 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1772
1773 workspace
1774 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1775 .await
1776 .unwrap();
1777 assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
1778
1779 workspace
1780 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1781 .await
1782 .unwrap();
1783 assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
1784
1785 workspace
1786 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1787 .await
1788 .unwrap();
1789 assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
1790
1791 workspace
1792 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1793 .await
1794 .unwrap();
1795 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1796
1797 workspace
1798 .update(cx, |workspace, cx| Pane::go_back(workspace, None, cx))
1799 .await
1800 .unwrap();
1801 assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
1802
1803 fn active_path(
1804 workspace: &ViewHandle<Workspace>,
1805 cx: &TestAppContext,
1806 ) -> Option<ProjectPath> {
1807 workspace.read_with(cx, |workspace, cx| {
1808 let item = workspace.active_item(cx)?;
1809 item.project_path(cx)
1810 })
1811 }
1812 }
1813
1814 #[gpui::test]
1815 fn test_bundled_settings_and_themes(cx: &mut AppContext) {
1816 cx.platform()
1817 .fonts()
1818 .add_fonts(&[
1819 Assets
1820 .load("fonts/zed-sans/zed-sans-extended.ttf")
1821 .unwrap()
1822 .to_vec()
1823 .into(),
1824 Assets
1825 .load("fonts/zed-mono/zed-mono-extended.ttf")
1826 .unwrap()
1827 .to_vec()
1828 .into(),
1829 ])
1830 .unwrap();
1831 let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
1832 let settings = Settings::defaults(Assets, cx.font_cache(), &themes);
1833
1834 let mut has_default_theme = false;
1835 for theme_name in themes.list(false).map(|meta| meta.name) {
1836 let theme = themes.get(&theme_name).unwrap();
1837 if theme.meta.name == settings.theme.meta.name {
1838 has_default_theme = true;
1839 }
1840 assert_eq!(theme.meta.name, theme_name);
1841 }
1842 assert!(has_default_theme);
1843 }
1844
1845 #[gpui::test]
1846 fn test_bundled_languages(cx: &mut AppContext) {
1847 let mut languages = LanguageRegistry::test();
1848 languages.set_executor(cx.background().clone());
1849 let languages = Arc::new(languages);
1850 let themes = ThemeRegistry::new((), cx.font_cache().clone());
1851 let http = FakeHttpClient::with_404_response();
1852 let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
1853 languages::init(languages.clone(), themes, node_runtime);
1854 for name in languages.language_names() {
1855 languages.language_for_name(&name);
1856 }
1857 cx.foreground().run_until_parked();
1858 }
1859
1860 fn init(cx: &mut TestAppContext) -> Arc<AppState> {
1861 cx.foreground().forbid_parking();
1862 cx.update(|cx| {
1863 let mut app_state = AppState::test(cx);
1864 let state = Arc::get_mut(&mut app_state).unwrap();
1865 state.initialize_workspace = initialize_workspace;
1866 state.build_window_options = build_window_options;
1867 call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
1868 workspace::init(app_state.clone(), cx);
1869 editor::init(cx);
1870 pane::init(cx);
1871 app_state
1872 })
1873 }
1874
1875 fn rust_lang() -> Arc<language::Language> {
1876 Arc::new(language::Language::new(
1877 language::LanguageConfig {
1878 name: "Rust".into(),
1879 path_suffixes: vec!["rs".to_string()],
1880 ..Default::default()
1881 },
1882 Some(tree_sitter_rust::language()),
1883 ))
1884 }
1885}