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