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