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