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