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