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