diff --git a/Cargo.lock b/Cargo.lock index 1ff9981a6a..a0be9756bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3172,7 +3172,6 @@ dependencies = [ name = "gpui_macros" version = "0.1.0" dependencies = [ - "gpui", "proc-macro2", "quote", "syn 1.0.109", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 3b894de723..d0fad11503 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -265,7 +265,6 @@ "alt-enter": "search::SelectAllMatches", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-r": "search::ActivateRegexMode", "alt-tab": "search::CycleMode", "alt-cmd-f": "project_search::ToggleFilters" } diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 94a271f037..02c09b33af 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -101,6 +101,8 @@ "vim::SwitchMode", "Normal" ], + "v": "vim::ToggleVisual", + "shift-v": "vim::ToggleVisualLine", "*": "vim::MoveToNext", "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion @@ -236,6 +238,14 @@ "ctrl-w ctrl-q": "pane::CloseAllItems" } }, + { + // escape is in its own section so that it cancels a pending count. + "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", + "bindings": { + "escape": "editor::Cancel", + "ctrl+[": "editor::Cancel" + } + }, { "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting", "bindings": { @@ -266,22 +276,6 @@ "o": "vim::InsertLineBelow", "shift-o": "vim::InsertLineAbove", "~": "vim::ChangeCase", - "v": [ - "vim::SwitchMode", - { - "Visual": { - "line": false - } - } - ], - "shift-v": [ - "vim::SwitchMode", - { - "Visual": { - "line": true - } - } - ], "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", @@ -374,12 +368,14 @@ "context": "Editor && vim_mode == visual && !VimWaiting", "bindings": { "u": "editor::Undo", - "c": "vim::VisualChange", + "o": "vim::OtherEnd", + "shift-o": "vim::OtherEnd", "d": "vim::VisualDelete", "x": "vim::VisualDelete", "y": "vim::VisualYank", "p": "vim::VisualPaste", "s": "vim::Substitute", + "c": "vim::Substitute", "~": "vim::ChangeCase", "r": [ "vim::PushOperator", @@ -389,6 +385,14 @@ "vim::SwitchMode", "Normal" ], + "escape": [ + "vim::SwitchMode", + "Normal" + ], + "ctrl+[": [ + "vim::SwitchMode", + "Normal" + ], ">": "editor::Indent", "<": "editor::Outdent" } diff --git a/assets/settings/default.json b/assets/settings/default.json index 397dac0961..c6235e80a1 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -214,7 +214,9 @@ "copilot": { // The set of glob patterns for which copilot should be disabled // in any matching file. - "disabled_globs": [".env"] + "disabled_globs": [ + ".env" + ] }, // Settings specific to journaling "journal": { diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8b46d7cfc5..6d1db5ada5 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -318,7 +318,7 @@ impl View for ActivityIndicator { on_click, } = self.content_to_render(cx); - let mut element = MouseEventHandler::::new(0, cx, |state, cx| { + let mut element = MouseEventHandler::new::(0, cx, |state, cx| { let theme = &theme::current(cx).workspace.status_bar.lsp_status; let style = if state.hovered() && on_click.is_some() { theme.hovered.as_ref().unwrap_or(&theme.default) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index d8fa42d6d4..bced6021ba 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -348,7 +348,7 @@ impl AssistantPanel { enum History {} let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.hamburger_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -380,7 +380,7 @@ impl AssistantPanel { fn render_split_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.split_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -404,7 +404,7 @@ impl AssistantPanel { fn render_assist_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.assist_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -422,7 +422,7 @@ impl AssistantPanel { fn render_quote_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.quote_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -450,7 +450,7 @@ impl AssistantPanel { fn render_plus_button(cx: &mut ViewContext) -> impl Element { let theme = theme::current(cx); let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.assistant.plus_button.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -480,7 +480,7 @@ impl AssistantPanel { &theme.assistant.zoom_in_button }; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Svg::for_style(style.icon.clone()) .contained() @@ -506,7 +506,7 @@ impl AssistantPanel { ) -> impl Element { let conversation = &self.saved_conversations[index]; let path = conversation.path.clone(); - MouseEventHandler::::new(index, cx, move |state, cx| { + MouseEventHandler::new::(index, cx, move |state, cx| { let style = &theme::current(cx).assistant.saved_conversation; Flex::row() .with_child( @@ -1818,7 +1818,7 @@ impl ConversationEditor { let theme = theme::current(cx); let style = &theme.assistant; let message_id = message.id; - let sender = MouseEventHandler::::new( + let sender = MouseEventHandler::new::( message_id.0, cx, |state, _| match message.role { @@ -2044,7 +2044,7 @@ impl ConversationEditor { ) -> impl Element { enum Model {} - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let style = style.model.style_for(state); Label::new(self.conversation.read(cx).model.clone(), style.text.clone()) .contained() diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index cd2e53905d..8397fa0745 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -31,7 +31,7 @@ impl View for UpdateNotification { let app_name = cx.global::().display_name(); - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { Flex::column() .with_child( Flex::row() @@ -48,7 +48,7 @@ impl View for UpdateNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 433dbed29b..615e238648 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -82,7 +82,7 @@ impl View for Breadcrumbs { .into_any(); } - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); crumbs.with_style(style.container) }) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index ce7fd8a094..657457d592 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7953,7 +7953,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Host editor update the cache version after every cache/view change", ); }); @@ -7976,7 +7977,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest editor update the cache version after every cache/view change" ); }); @@ -7996,7 +7998,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Host should get hints from the 1st edit and 1st LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -8010,7 +8012,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints the 1st edit and 2nd LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_a.update(cx_a, |editor, cx| { @@ -8035,7 +8037,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( 4th query was made by guest (but not applied) due to cache invalidation logic" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -8051,7 +8053,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints from 3rd edit, 6th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); fake_language_server @@ -8077,7 +8079,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Host should accepted all edits and bump its cache version every time" ); }); @@ -8098,7 +8101,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, + inlay_cache.version(), edits_made, "Guest should accepted all edits and bump its cache version every time" ); @@ -8264,7 +8267,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 0, + inlay_cache.version(), + 0, "Host should not increment its cache version due to no changes", ); }); @@ -8279,7 +8283,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest editor update the cache version after every cache/view change" ); }); @@ -8296,7 +8301,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 0, + inlay_cache.version(), + 0, "Host should not increment its cache version due to no changes", ); }); @@ -8311,7 +8317,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest should accepted all edits and bump its cache version every time" ); }); @@ -8343,13 +8350,10 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP fn extract_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); - for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - for (_, inlay) in excerpt_hints.hints.iter() { - match &inlay.label { - project::InlayHintLabel::String(s) => labels.push(s.to_string()), - _ => unreachable!(), - } + for hint in editor.inlay_hint_cache().hints() { + match hint.label { + project::InlayHintLabel::String(s) => labels.push(s), + _ => unreachable!(), } } labels diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index fefb1c608f..bda11796e0 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -226,7 +226,7 @@ impl CollabTitlebarItem { let mut ret = Flex::row().with_child( Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |mouse_state, cx| { + MouseEventHandler::new::(0, cx, |mouse_state, cx| { let style = project_style .in_state(self.project_popover.is_some()) .style_for(mouse_state); @@ -266,7 +266,7 @@ impl CollabTitlebarItem { .with_child( Stack::new() .with_child( - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |mouse_state, cx| { @@ -398,7 +398,7 @@ impl CollabTitlebarItem { self.branch_popover.as_ref().map(|child| { let theme = theme::current(cx).clone(); let child = ChildView::new(child, cx); - let child = MouseEventHandler::::new(0, cx, |_, _| { + let child = MouseEventHandler::new::(0, cx, |_, _| { child .flex(1., true) .contained() @@ -433,7 +433,7 @@ impl CollabTitlebarItem { self.project_popover.as_ref().map(|child| { let theme = theme::current(cx).clone(); let child = ChildView::new(child, cx); - let child = MouseEventHandler::::new(0, cx, |_, _| { + let child = MouseEventHandler::new::(0, cx, |_, _| { child .flex(1., true) .contained() @@ -560,7 +560,7 @@ impl CollabTitlebarItem { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_contacts_button .in_state(self.contacts_popover.is_some()) @@ -610,7 +610,7 @@ impl CollabTitlebarItem { let active = room.read(cx).is_screen_sharing(); let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .screen_share_button .in_state(active) @@ -659,7 +659,7 @@ impl CollabTitlebarItem { } let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_microphone_button .in_state(is_muted) @@ -712,7 +712,7 @@ impl CollabTitlebarItem { } let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar .toggle_speakers_button .in_state(is_deafened) @@ -747,7 +747,7 @@ impl CollabTitlebarItem { let tooltip = "Leave call"; let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar.leave_call_button.style_for(state); Svg::new(icon) .with_color(style.color) @@ -801,7 +801,7 @@ impl CollabTitlebarItem { Some( Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { //TODO: Ensure this button has consistent width for both text variations let style = titlebar.share_button.inactive_state().style_for(state); Label::new(label, style.text.clone()) @@ -847,7 +847,7 @@ impl CollabTitlebarItem { let avatar_style = &user_menu_button_style.avatar; Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = user_menu_button_style .user_menu .inactive_state() @@ -907,7 +907,7 @@ impl CollabTitlebarItem { fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { let titlebar = &theme.titlebar; - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = titlebar.sign_in_button.inactive_state().style_for(state); Label::new("Sign In", style.text.clone()) .contained() @@ -1142,7 +1142,7 @@ impl CollabTitlebarItem { if let Some(replica_id) = replica_id { enum ToggleFollow {} - content = MouseEventHandler::::new( + content = MouseEventHandler::new::( replica_id.into(), cx, move |_, _| content, @@ -1173,7 +1173,7 @@ impl CollabTitlebarItem { enum JoinProject {} let user_id = user.id; - content = MouseEventHandler::::new( + content = MouseEventHandler::new::( peer_id.as_u64() as usize, cx, move |_, _| content, @@ -1261,7 +1261,7 @@ impl CollabTitlebarItem { .into_any(), ), client::Status::UpgradeRequired => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Label::new( "Please update Zed to collaborate", theme.titlebar.outdated_warning.text.clone(), diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index b8024e2bfd..83f3bd97b2 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -810,7 +810,7 @@ impl ContactList { worktree_root_names.join(", ") }; - MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -904,7 +904,7 @@ impl ContactList { let baseline_offset = row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - MouseEventHandler::::new( + MouseEventHandler::new::( peer_id.as_u64() as usize, cx, |mouse_state, _| { @@ -1006,7 +1006,7 @@ impl ContactList { }; let leave_call = if section == Section::ActiveCall { Some( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.leave_call.style_for(state); Label::new("Leave Call", style.text.clone()) .contained() @@ -1024,7 +1024,7 @@ impl ContactList { }; let icon_size = theme.section_icon_size; - MouseEventHandler::::new(section as usize, cx, |_, _| { + MouseEventHandler::new::(section as usize, cx, |_, _| { Flex::row() .with_child( Svg::new(if is_collapsed { @@ -1075,7 +1075,7 @@ impl ContactList { let github_login = contact.user.github_login.clone(); let initial_project = project.clone(); let mut event_handler = - MouseEventHandler::::new(contact.user.id as usize, cx, |_, cx| { + MouseEventHandler::new::(contact.user.id as usize, cx, |_, cx| { Flex::row() .with_children(contact.user.avatar.clone().map(|avatar| { let status_badge = if contact.online { @@ -1114,7 +1114,7 @@ impl ContactList { .flex(1., true), ) .with_child( - MouseEventHandler::::new( + MouseEventHandler::new::( contact.user.id as usize, cx, |mouse_state, _| { @@ -1208,7 +1208,7 @@ impl ContactList { if is_incoming { row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1231,7 +1231,7 @@ impl ContactList { ); row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1254,7 +1254,7 @@ impl ContactList { ); } else { row.add_child( - MouseEventHandler::::new(user.id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { let button_style = if is_contact_request_pending { &theme.disabled_button } else { @@ -1333,7 +1333,7 @@ impl View for ContactList { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { render_icon_button( &theme.contact_list.add_contact_button, "icons/user_plus_16.svg", diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index 1d6d1c84c7..39ab9c621c 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -113,7 +113,7 @@ impl View for ContactsPopover { Child::ContactFinder(child) => ChildView::new(child, cx), }; - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Flex::column() .with_child(child.flex(1., true)) .contained() diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 6f86a74300..410adbf862 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -173,7 +173,7 @@ impl IncomingCallNotification { let theme = theme::current(cx); Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.incoming_call_notification; Label::new("Accept", theme.accept_button.text.clone()) .aligned() @@ -187,7 +187,7 @@ impl IncomingCallNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.incoming_call_notification; Label::new("Decline", theme.decline_button.text.clone()) .aligned() diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index cbd072fe89..9258ad3ab1 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -52,7 +52,7 @@ where .flex(1., true), ) .with_child( - MouseEventHandler::::new(user.id as usize, cx, |state, _| { + MouseEventHandler::new::(user.id as usize, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) @@ -92,7 +92,7 @@ where Flex::row() .with_children(buttons.into_iter().enumerate().map( |(ix, (message, handler))| { - MouseEventHandler::::new(ix, cx, |state, _| { + MouseEventHandler::new::(ix, cx, |state, _| { let button = theme.button.style_for(state); Label::new(message, button.text.clone()) .contained() diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 63922f2b65..500599db59 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -170,7 +170,7 @@ impl ProjectSharedNotification { let theme = theme::current(cx); Flex::column() .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.project_shared_notification; Label::new("Open", theme.open_button.text.clone()) .aligned() @@ -182,7 +182,7 @@ impl ProjectSharedNotification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { let theme = &theme.project_shared_notification; Label::new("Dismiss", theme.dismiss_button.text.clone()) .aligned() diff --git a/crates/collab_ui/src/sharing_status_indicator.rs b/crates/collab_ui/src/sharing_status_indicator.rs index a39ffc457a..9fcd15aa18 100644 --- a/crates/collab_ui/src/sharing_status_indicator.rs +++ b/crates/collab_ui/src/sharing_status_indicator.rs @@ -47,7 +47,7 @@ impl View for SharingStatusIndicator { Appearance::Dark | Appearance::VibrantDark => Color::white(), }; - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Svg::new("icons/disable_screen_sharing_12.svg") .with_color(color) .constrained() diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index a5534b6262..89df86beef 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -439,14 +439,14 @@ impl ContextMenu { let style = theme::current(cx).context_menu.clone(); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Flex::column() .with_children(self.items.iter().enumerate().map(|(ix, item)| { match item { ContextMenuItem::Item { label, action } => { let action = action.clone(); let view_id = self.parent_view_id; - MouseEventHandler::::new(ix, cx, |state, _| { + MouseEventHandler::new::(ix, cx, |state, _| { let style = style.item.in_state(self.selected_index == Some(ix)); let style = style.style_for(state); let keystroke = match &action { diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index d03a2d393b..ac3b81f0c6 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -113,7 +113,7 @@ impl CopilotCodeVerification { let device_code_style = &style.auth.prompting.device_code; - MouseEventHandler::::new(0, cx, |state, _cx| { + MouseEventHandler::new::(0, cx, |state, _cx| { Flex::row() .with_child( Label::new(data.user_code.clone(), device_code_style.text.clone()) diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index eae1746a01..f73f854927 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -62,7 +62,7 @@ impl View for CopilotButton { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, { + MouseEventHandler::new::(0, cx, { let theme = theme.clone(); let status = status.clone(); move |state, _cx| { diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 0ae55e99d9..d1a32c72f1 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -94,7 +94,7 @@ impl View for DiagnosticIndicator { let tooltip_style = theme::current(cx).tooltip.clone(); let in_progress = !self.in_progress_checks.is_empty(); let mut element = Flex::row().with_child( - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let theme = theme::current(cx); let style = theme .workspace @@ -195,7 +195,7 @@ impl View for DiagnosticIndicator { } else if let Some(diagnostic) = &self.current_diagnostic { let message_style = style.diagnostic_message.clone(); element.add_child( - MouseEventHandler::::new(1, cx, |state, _| { + MouseEventHandler::new::(1, cx, |state, _| { Label::new( diagnostic.message.split('\n').next().unwrap().to_string(), message_style.style_for(state).text.clone(), diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index ddfed0c858..59b0bc89e2 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -202,7 +202,7 @@ impl DragAndDrop { let position = (position - region_offset).round(); Some( Overlay::new( - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |_, cx| render(payload, cx), @@ -235,7 +235,7 @@ impl DragAndDrop { } State::Canceled => Some( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Empty::new().constrained().with_width(0.).with_height(0.) }) .on_up(MouseButton::Left, |_, _, cx| { @@ -301,7 +301,7 @@ pub trait Draggable { Self: Sized; } -impl Draggable for MouseEventHandler { +impl Draggable for MouseEventHandler { fn as_draggable( self, payload: P, diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6e04833f17..aee41e6c53 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -353,19 +353,26 @@ impl DisplaySnapshot { } } + // used by line_mode selections and tries to match vim behaviour pub fn expand_to_line(&self, range: Range) -> Range { - let mut new_start = self.prev_line_boundary(range.start).0; - let mut new_end = self.next_line_boundary(range.end).0; + let new_start = if range.start.row == 0 { + Point::new(0, 0) + } else if range.start.row == self.max_buffer_row() + || (range.end.column > 0 && range.end.row == self.max_buffer_row()) + { + Point::new(range.start.row - 1, self.line_len(range.start.row - 1)) + } else { + self.prev_line_boundary(range.start).0 + }; - if new_start.row == range.start.row && new_end.row == range.end.row { - if new_end.row < self.buffer_snapshot.max_point().row { - new_end.row += 1; - new_end.column = 0; - } else if new_start.row > 0 { - new_start.row -= 1; - new_start.column = self.buffer_snapshot.line_len(new_start.row); - } - } + let new_end = if range.end.column == 0 { + range.end + } else if range.end.row < self.max_buffer_row() { + self.buffer_snapshot + .clip_point(Point::new(range.end.row + 1, 0), Bias::Left) + } else { + self.buffer_snapshot.max_point() + }; new_start..new_end } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d8b77ea95..256ef2284c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -867,7 +867,7 @@ impl CompletionsMenu { let completion = &completions[mat.candidate_id]; let item_ix = start_ix + ix; items.push( - MouseEventHandler::::new( + MouseEventHandler::new::( mat.candidate_id, cx, |state, _| { @@ -1044,7 +1044,7 @@ impl CodeActionsMenu { for (ix, action) in actions[range].iter().enumerate() { let item_ix = start_ix + ix; items.push( - MouseEventHandler::::new(item_ix, cx, |state, _| { + MouseEventHandler::new::(item_ix, cx, |state, _| { let item_style = if item_ix == selected_item { style.autocomplete.selected_item } else if state.hovered() { @@ -2723,7 +2723,7 @@ impl Editor { .collect() } - fn excerpt_visible_offsets( + pub fn excerpt_visible_offsets( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext<'_, '_, Editor>, @@ -3547,7 +3547,7 @@ impl Editor { if self.available_code_actions.is_some() { enum CodeActions {} Some( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { Svg::new("icons/bolt_8.svg").with_color( style .code_actions @@ -3594,7 +3594,7 @@ impl Editor { fold_data .map(|(fold_status, buffer_row, active)| { (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { - MouseEventHandler::::new( + MouseEventHandler::new::( ix as usize, cx, |mouse_state, _| { @@ -8663,7 +8663,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); let anchor_x = cx.anchor_x; enum BlockContextToolip {} - MouseEventHandler::::new(cx.block_id, cx, |_, _| { + MouseEventHandler::new::(cx.block_id, cx, |_, _| { Flex::column() .with_children(highlighted_lines.iter().map(|(line, highlights)| { Label::new( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 13856cc8ef..04f45921d7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -63,6 +63,7 @@ struct SelectionLayout { cursor_shape: CursorShape, is_newest: bool, range: Range, + active_rows: Range, } impl SelectionLayout { @@ -73,25 +74,44 @@ impl SelectionLayout { map: &DisplaySnapshot, is_newest: bool, ) -> Self { + let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); + let display_selection = point_selection.map(|p| p.to_display_point(map)); + let mut range = display_selection.range(); + let mut head = display_selection.head(); + let mut active_rows = map.prev_line_boundary(point_selection.start).1.row() + ..map.next_line_boundary(point_selection.end).1.row(); + + // vim visual line mode if line_mode { - let selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); - let point_range = map.expand_to_line(selection.range()); - Self { - head: selection.head().to_display_point(map), - cursor_shape, - is_newest, - range: point_range.start.to_display_point(map) - ..point_range.end.to_display_point(map), - } - } else { - let selection = selection.map(|p| p.to_display_point(map)); - Self { - head: selection.head(), - cursor_shape, - is_newest, - range: selection.range(), + let point_range = map.expand_to_line(point_selection.range()); + range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map); + } + + // any vim visual mode (including line mode) + if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed { + if head.column() > 0 { + head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left) + } else if head.row() > 0 && head != map.max_point() { + head = map.clip_point( + DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)), + Bias::Left, + ); + // updating range.end is a no-op unless you're cursor is + // on the newline containing a multi-buffer divider + // in which case the clip_point may have moved the head up + // an additional row. + range.end = DisplayPoint::new(head.row() + 1, 0); + active_rows.end = head.row(); } } + + Self { + head, + cursor_shape, + is_newest, + range, + active_rows, + } } } @@ -1637,7 +1657,7 @@ impl EditorElement { let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); enum JumpIcon {} - MouseEventHandler::::new((*id).into(), cx, |state, _| { + MouseEventHandler::new::((*id).into(), cx, |state, _| { let style = style.jump_icon.style_for(state); Svg::new("icons/arrow_up_right_8.svg") .with_color(style.color) @@ -2152,22 +2172,37 @@ impl Element for EditorElement { } selections.extend(remote_selections); + let mut newest_selection_head = None; + if editor.show_local_selections { - let mut local_selections = editor + let mut local_selections: Vec> = editor .selections .disjoint_in_range(start_anchor..end_anchor, cx); local_selections.extend(editor.selections.pending(cx)); + let mut layouts = Vec::new(); let newest = editor.selections.newest(cx); - for selection in &local_selections { + for selection in local_selections.drain(..) { let is_empty = selection.start == selection.end; - let selection_start = snapshot.prev_line_boundary(selection.start).1; - let selection_end = snapshot.next_line_boundary(selection.end).1; - for row in cmp::max(selection_start.row(), start_row) - ..=cmp::min(selection_end.row(), end_row) + let is_newest = selection == newest; + + let layout = SelectionLayout::new( + selection, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + is_newest, + ); + if is_newest { + newest_selection_head = Some(layout.head); + } + + for row in cmp::max(layout.active_rows.start, start_row) + ..=cmp::min(layout.active_rows.end, end_row) { let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); *contains_non_empty_selection |= !is_empty; } + layouts.push(layout); } // Render the local selections in the leader's color when following. @@ -2175,22 +2210,7 @@ impl Element for EditorElement { .leader_replica_id .unwrap_or_else(|| editor.replica_id(cx)); - selections.push(( - local_replica_id, - local_selections - .into_iter() - .map(|selection| { - let is_newest = selection == newest; - SelectionLayout::new( - selection, - editor.selections.line_mode, - editor.cursor_shape, - &snapshot.display_snapshot, - is_newest, - ) - }) - .collect(), - )); + selections.push((local_replica_id, layouts)); } let scrollbar_settings = &settings::get::(cx).scrollbar; @@ -2295,28 +2315,26 @@ impl Element for EditorElement { snapshot = editor.snapshot(cx); } - let newest_selection_head = editor - .selections - .newest::(cx) - .head() - .to_display_point(&snapshot); let style = editor.style(cx); let mut context_menu = None; let mut code_actions_indicator = None; - if (start_row..end_row).contains(&newest_selection_head.row()) { - if editor.context_menu_visible() { - context_menu = editor.render_context_menu(newest_selection_head, style.clone(), cx); + if let Some(newest_selection_head) = newest_selection_head { + if (start_row..end_row).contains(&newest_selection_head.row()) { + if editor.context_menu_visible() { + context_menu = + editor.render_context_menu(newest_selection_head, style.clone(), cx); + } + + let active = matches!( + editor.context_menu, + Some(crate::ContextMenu::CodeActions(_)) + ); + + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|indicator| (newest_selection_head.row(), indicator)); } - - let active = matches!( - editor.context_menu, - Some(crate::ContextMenu::CodeActions(_)) - ); - - code_actions_indicator = editor - .render_code_actions_indicator(&style, active, cx) - .map(|indicator| (newest_selection_head.row(), indicator)); } let visible_rows = start_row..start_row + line_layouts.len() as u32; @@ -2995,6 +3013,154 @@ mod tests { assert_eq!(layouts.len(), 6); } + #[gpui::test] + async fn test_vim_visual_selections(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }) + .root(cx); + let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + let (_, state) = editor.update(cx, |editor, cx| { + editor.cursor_shape = CursorShape::Block; + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 0)..Point::new(1, 0), + Point::new(3, 2)..Point::new(3, 3), + Point::new(5, 6)..Point::new(6, 0), + ]); + }); + let mut new_parents = Default::default(); + let mut notify_views_if_parents_change = Default::default(); + let mut layout_cx = LayoutContext::new( + cx, + &mut new_parents, + &mut notify_views_if_parents_change, + false, + ); + element.layout( + SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), + editor, + &mut layout_cx, + ) + }); + assert_eq!(state.selections.len(), 1); + let local_selections = &state.selections[0].1; + assert_eq!(local_selections.len(), 3); + // moves cursor back one line + assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6)); + assert_eq!( + local_selections[0].range, + DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0) + ); + + // moves cursor back one column + assert_eq!( + local_selections[1].range, + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3) + ); + assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2)); + + // leaves cursor on the max point + assert_eq!( + local_selections[2].range, + DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0) + ); + assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0)); + + // active lines does not include 1 (even though the range of the selection does) + assert_eq!( + state.active_rows.keys().cloned().collect::>(), + vec![0, 3, 5, 6] + ); + + // multi-buffer support + // in DisplayPoint co-ordinates, this is what we're dealing with: + // 0: [[file + // 1: header]] + // 2: aaaaaa + // 3: bbbbbb + // 4: cccccc + // 5: + // 6: ... + // 7: ffffff + // 8: gggggg + // 9: hhhhhh + // 10: + // 11: [[file + // 12: header]] + // 13: bbbbbb + // 14: cccccc + // 15: dddddd + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_multi( + [ + ( + &(sample_text(8, 6, 'a') + "\n"), + vec![ + Point::new(0, 0)..Point::new(3, 0), + Point::new(4, 0)..Point::new(7, 0), + ], + ), + ( + &(sample_text(8, 6, 'a') + "\n"), + vec![Point::new(1, 0)..Point::new(3, 0)], + ), + ], + cx, + ); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }) + .root(cx); + let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); + let (_, state) = editor.update(cx, |editor, cx| { + editor.cursor_shape = CursorShape::Block; + editor.change_selections(None, cx, |s| { + s.select_display_ranges([ + DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0), + DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0), + ]); + }); + let mut new_parents = Default::default(); + let mut notify_views_if_parents_change = Default::default(); + let mut layout_cx = LayoutContext::new( + cx, + &mut new_parents, + &mut notify_views_if_parents_change, + false, + ); + element.layout( + SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)), + editor, + &mut layout_cx, + ) + }); + + assert_eq!(state.selections.len(), 1); + let local_selections = &state.selections[0].1; + assert_eq!(local_selections.len(), 2); + + // moves cursor on excerpt boundary back a line + // and doesn't allow selection to bleed through + assert_eq!( + local_selections[0].range, + DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0) + ); + assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0)); + + // moves cursor on buffer boundary back two lines + // and doesn't allow selection to bleed through + assert_eq!( + local_selections[1].range, + DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0) + ); + assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0)); + } + #[gpui::test] fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index afc13f983d..e4509a765c 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -565,7 +565,7 @@ impl InfoPopover { ) }); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let mut region_id = 0; let view_id = cx.view_id(); @@ -654,7 +654,7 @@ impl DiagnosticPopover { let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { text.with_soft_wrap(true) .contained() .with_style(container_style) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 2d75b4d2ce..8be72aec46 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::Context; use clock::Global; -use gpui::{ModelHandle, Task, ViewContext}; +use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; use parking_lot::RwLock; @@ -17,14 +17,21 @@ use project::InlayHint; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use sum_tree::Bias; use util::post_inc; pub struct InlayHintCache { - pub hints: HashMap>>, - pub allowed_hint_kinds: HashSet>, - pub version: usize, - pub enabled: bool, - update_tasks: HashMap, + hints: HashMap>>, + allowed_hint_kinds: HashSet>, + version: usize, + enabled: bool, + update_tasks: HashMap, +} + +#[derive(Debug)] +struct TasksForRanges { + tasks: Vec>, + sorted_ranges: Vec>, } #[derive(Debug)] @@ -32,7 +39,7 @@ pub struct CachedExcerptHints { version: usize, buffer_version: Global, buffer_id: u64, - pub hints: Vec<(InlayId, InlayHint)>, + hints: Vec<(InlayId, InlayHint)>, } #[derive(Debug, Clone, Copy)] @@ -48,18 +55,6 @@ pub struct InlaySplice { pub to_insert: Vec, } -struct UpdateTask { - invalidate: InvalidationStrategy, - cache_version: usize, - task: RunningTask, - pending_refresh: Option, -} - -struct RunningTask { - _task: Task<()>, - is_running_rx: smol::channel::Receiver<()>, -} - #[derive(Debug)] struct ExcerptHintsUpdate { excerpt_id: ExcerptId, @@ -72,24 +67,10 @@ struct ExcerptHintsUpdate { struct ExcerptQuery { buffer_id: u64, excerpt_id: ExcerptId, - dimensions: ExcerptDimensions, cache_version: usize, invalidate: InvalidationStrategy, } -#[derive(Debug, Clone, Copy)] -struct ExcerptDimensions { - excerpt_range_start: language::Anchor, - excerpt_range_end: language::Anchor, - excerpt_visible_range_start: language::Anchor, - excerpt_visible_range_end: language::Anchor, -} - -struct HintFetchRanges { - visible_range: Range, - other_ranges: Vec>, -} - impl InvalidationStrategy { fn should_invalidate(&self) -> bool { matches!( @@ -99,35 +80,92 @@ impl InvalidationStrategy { } } -impl ExcerptQuery { - fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges { - let visible_range = - self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end; - let mut other_ranges = Vec::new(); - if self - .dimensions - .excerpt_range_start - .cmp(&visible_range.start, buffer) - .is_lt() - { - let mut end = visible_range.start; - end.offset -= 1; - other_ranges.push(self.dimensions.excerpt_range_start..end); - } - if self - .dimensions - .excerpt_range_end - .cmp(&visible_range.end, buffer) - .is_gt() - { - let mut start = visible_range.end; - start.offset += 1; - other_ranges.push(start..self.dimensions.excerpt_range_end); +impl TasksForRanges { + fn new(sorted_ranges: Vec>, task: Task<()>) -> Self { + Self { + tasks: vec![task], + sorted_ranges, } + } - HintFetchRanges { - visible_range, - other_ranges: other_ranges.into_iter().map(|range| range).collect(), + fn update_cached_tasks( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + invalidate: InvalidationStrategy, + spawn_task: impl FnOnce(Vec>) -> Task<()>, + ) { + let ranges_to_query = match invalidate { + InvalidationStrategy::None => { + let mut ranges_to_query = Vec::new(); + let mut latest_cached_range = None::<&mut Range>; + for cached_range in self + .sorted_ranges + .iter_mut() + .skip_while(|cached_range| { + cached_range + .end + .cmp(&query_range.start, buffer_snapshot) + .is_lt() + }) + .take_while(|cached_range| { + cached_range + .start + .cmp(&query_range.end, buffer_snapshot) + .is_le() + }) + { + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) + < cached_range.start.offset + { + ranges_to_query.push(latest_cached_range.end..cached_range.start); + cached_range.start = latest_cached_range.end; + } + } + None => { + if query_range + .start + .cmp(&cached_range.start, buffer_snapshot) + .is_lt() + { + ranges_to_query.push(query_range.start..cached_range.start); + cached_range.start = query_range.start; + } + } + } + latest_cached_range = Some(cached_range); + } + + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset + { + ranges_to_query.push(latest_cached_range.end..query_range.end); + latest_cached_range.end = query_range.end; + } + } + None => { + ranges_to_query.push(query_range.clone()); + self.sorted_ranges.push(query_range); + self.sorted_ranges.sort_by(|range_a, range_b| { + range_a.start.cmp(&range_b.start, buffer_snapshot) + }); + } + } + + ranges_to_query + } + InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { + self.tasks.clear(); + self.sorted_ranges.clear(); + vec![query_range] + } + }; + + if !ranges_to_query.is_empty() { + self.tasks.push(spawn_task(ranges_to_query)); } } } @@ -168,7 +206,6 @@ impl InlayHintCache { ); if new_splice.is_some() { self.version += 1; - self.update_tasks.clear(); self.allowed_hint_kinds = new_allowed_hint_kinds; } ControlFlow::Break(new_splice) @@ -197,7 +234,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, - mut excerpts_to_query: HashMap, Global, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, ) -> Option { @@ -205,43 +242,23 @@ impl InlayHintCache { return None; } - let update_tasks = &mut self.update_tasks; let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { - let mut changed = false; - update_tasks.retain(|task_excerpt_id, _| { - let retain = excerpts_to_query.contains_key(task_excerpt_id); - changed |= !retain; - retain - }); + self.update_tasks + .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); self.hints.retain(|cached_excerpt, cached_hints| { let retain = excerpts_to_query.contains_key(cached_excerpt); - changed |= !retain; if !retain { invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id)); } retain }); - if changed { - self.version += 1; - } } if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { return None; } - let cache_version = self.version; - excerpts_to_query.retain(|visible_excerpt_id, _| { - match update_tasks.entry(*visible_excerpt_id) { - hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) { - cmp::Ordering::Less => true, - cmp::Ordering::Equal => invalidate.should_invalidate(), - cmp::Ordering::Greater => false, - }, - hash_map::Entry::Vacant(_) => true, - } - }); - + let cache_version = self.version + 1; cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { @@ -368,6 +385,19 @@ impl InlayHintCache { self.update_tasks.clear(); self.hints.clear(); } + + pub fn hints(&self) -> Vec { + let mut hints = Vec::new(); + for excerpt_hints in self.hints.values() { + let excerpt_hints = excerpt_hints.read(); + hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned()); + } + hints + } + + pub fn version(&self) -> usize { + self.version + } } fn spawn_new_update_tasks( @@ -378,13 +408,14 @@ fn spawn_new_update_tasks( cx: &mut ViewContext<'_, '_, Editor>, ) { let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in + for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in excerpts_to_query { if excerpt_visible_range.is_empty() { continue; } - let buffer = buffer_handle.read(cx); + let buffer = excerpt_buffer.read(cx); + let buffer_id = buffer.remote_id(); let buffer_snapshot = buffer.snapshot(); if buffer_snapshot .version() @@ -402,202 +433,123 @@ fn spawn_new_update_tasks( { continue; } - if !new_task_buffer_version.changed_since(&cached_buffer_version) - && !matches!(invalidate, InvalidationStrategy::RefreshRequested) - { - continue; - } }; - let buffer_id = buffer.remote_id(); - let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start); - let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end); - - let (multi_buffer_snapshot, full_excerpt_range) = + let (multi_buffer_snapshot, Some(query_range)) = editor.buffer.update(cx, |multi_buffer, cx| { - let multi_buffer_snapshot = multi_buffer.snapshot(cx); ( - multi_buffer_snapshot, - multi_buffer - .excerpts_for_buffer(&buffer_handle, cx) - .into_iter() - .find(|(id, _)| id == &excerpt_id) - .map(|(_, range)| range.context), + multi_buffer.snapshot(cx), + determine_query_range( + multi_buffer, + excerpt_id, + &excerpt_buffer, + excerpt_visible_range, + cx, + ), ) - }); + }) else { return; }; + let query = ExcerptQuery { + buffer_id, + excerpt_id, + cache_version: update_cache_version, + invalidate, + }; - if let Some(full_excerpt_range) = full_excerpt_range { - let query = ExcerptQuery { - buffer_id, - excerpt_id, - dimensions: ExcerptDimensions { - excerpt_range_start: full_excerpt_range.start, - excerpt_range_end: full_excerpt_range.end, - excerpt_visible_range_start, - excerpt_visible_range_end, - }, - cache_version: update_cache_version, - invalidate, - }; + let new_update_task = |fetch_ranges| { + new_update_task( + query, + fetch_ranges, + multi_buffer_snapshot, + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints, + cx, + ) + }; - let new_update_task = |is_refresh_after_regular_task| { - new_update_task( - query, - multi_buffer_snapshot, - buffer_snapshot, - Arc::clone(&visible_hints), - cached_excerpt_hints, - is_refresh_after_regular_task, - cx, - ) - }; - match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { - hash_map::Entry::Occupied(mut o) => { - let update_task = o.get_mut(); - match (update_task.invalidate, invalidate) { - (_, InvalidationStrategy::None) => {} - ( - InvalidationStrategy::BufferEdited, - InvalidationStrategy::RefreshRequested, - ) if !update_task.task.is_running_rx.is_closed() => { - update_task.pending_refresh = Some(query); - } - _ => { - o.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } - } - } - hash_map::Entry::Vacant(v) => { - v.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { + hash_map::Entry::Occupied(mut o) => { + o.get_mut().update_cached_tasks( + &buffer_snapshot, + query_range, + invalidate, + new_update_task, + ); + } + hash_map::Entry::Vacant(v) => { + v.insert(TasksForRanges::new( + vec![query_range.clone()], + new_update_task(vec![query_range]), + )); } } } } +fn determine_query_range( + multi_buffer: &mut MultiBuffer, + excerpt_id: ExcerptId, + excerpt_buffer: &ModelHandle, + excerpt_visible_range: Range, + cx: &mut ModelContext<'_, MultiBuffer>, +) -> Option> { + let full_excerpt_range = multi_buffer + .excerpts_for_buffer(excerpt_buffer, cx) + .into_iter() + .find(|(id, _)| id == &excerpt_id) + .map(|(_, range)| range.context)?; + + let buffer = excerpt_buffer.read(cx); + let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; + let start_offset = excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range.start.offset); + let start = buffer.anchor_before(buffer.clip_offset(start_offset, Bias::Left)); + let end_offset = excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range.end.offset) + .min(buffer.len()); + let end = buffer.anchor_after(buffer.clip_offset(end_offset, Bias::Right)); + if start.cmp(&end, buffer).is_eq() { + None + } else { + Some(start..end) + } +} + fn new_update_task( query: ExcerptQuery, + hint_fetch_ranges: Vec>, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, cached_excerpt_hints: Option>>, - is_refresh_after_regular_task: bool, cx: &mut ViewContext<'_, '_, Editor>, -) -> RunningTask { - let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot); - let (is_running_tx, is_running_rx) = smol::channel::bounded(1); - let _task = cx.spawn(|editor, mut cx| async move { - let _is_running_tx = is_running_tx; - let create_update_task = |range| { - fetch_and_update_hints( - editor.clone(), - multi_buffer_snapshot.clone(), - buffer_snapshot.clone(), - Arc::clone(&visible_hints), - cached_excerpt_hints.as_ref().map(Arc::clone), - query, - range, - cx.clone(), - ) - }; - - if is_refresh_after_regular_task { - let visible_range_has_updates = - match create_update_task(hints_fetch_ranges.visible_range).await { - Ok(updated) => updated, - Err(e) => { - error!("inlay hint visible range update task failed: {e:#}"); - return; - } - }; - - if visible_range_has_updates { - let other_update_results = futures::future::join_all( - hints_fetch_ranges - .other_ranges - .into_iter() - .map(create_update_task), +) -> Task<()> { + cx.spawn(|editor, cx| async move { + let task_update_results = + futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + range, + cx.clone(), ) - .await; - - for result in other_update_results { - if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); - } - } - } - } else { - let task_update_results = futures::future::join_all( - std::iter::once(hints_fetch_ranges.visible_range) - .chain(hints_fetch_ranges.other_ranges.into_iter()) - .map(create_update_task), - ) + })) .await; - for result in task_update_results { - if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); - } + for result in task_update_results { + if let Err(e) = result { + error!("inlay hint update task failed: {e:#}"); } } - - editor - .update(&mut cx, |editor, cx| { - let pending_refresh_query = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) - .and_then(|task| task.pending_refresh.take()); - - if let Some(pending_refresh_query) = pending_refresh_query { - let refresh_multi_buffer = editor.buffer().read(cx); - let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx); - let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - let refresh_cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .get(&pending_refresh_query.excerpt_id) - .map(Arc::clone); - if let Some(buffer) = - refresh_multi_buffer.buffer(pending_refresh_query.buffer_id) - { - editor.inlay_hint_cache.update_tasks.insert( - pending_refresh_query.excerpt_id, - UpdateTask { - invalidate: InvalidationStrategy::RefreshRequested, - cache_version: editor.inlay_hint_cache.version, - task: new_update_task( - pending_refresh_query, - refresh_multi_buffer_snapshot, - buffer.read(cx).snapshot(), - refresh_visible_hints, - refresh_cached_excerpt_hints, - true, - cx, - ), - pending_refresh: None, - }, - ); - } - } - }) - .ok(); - }); - - RunningTask { - _task, - is_running_rx, - } + }) } async fn fetch_and_update_hints( @@ -609,7 +561,7 @@ async fn fetch_and_update_hints( query: ExcerptQuery, fetch_range: Range, mut cx: gpui::AsyncAppContext, -) -> anyhow::Result { +) -> anyhow::Result<()> { let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { editor @@ -625,11 +577,10 @@ async fn fetch_and_update_hints( }) .ok() .flatten(); - let mut update_happened = false; - let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) }; - let new_hints = inlay_hints_fetch_task - .await - .context("inlay hint fetch task")?; + let new_hints = match inlay_hints_fetch_task { + Some(task) => task.await.context("inlay hint fetch task")?, + None => return Ok(()), + }; let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx @@ -645,106 +596,21 @@ async fn fetch_and_update_hints( ) }) .await; - - editor - .update(&mut cx, |editor, cx| { - if let Some(new_update) = new_update { - update_happened = !new_update.add_to_cache.is_empty() - || !new_update.remove_from_cache.is_empty() - || !new_update.remove_from_visible.is_empty(); - - let cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .entry(new_update.excerpt_id) - .or_insert_with(|| { - Arc::new(RwLock::new(CachedExcerptHints { - version: query.cache_version, - buffer_version: buffer_snapshot.version().clone(), - buffer_id: query.buffer_id, - hints: Vec::new(), - })) - }); - let mut cached_excerpt_hints = cached_excerpt_hints.write(); - match query.cache_version.cmp(&cached_excerpt_hints.version) { - cmp::Ordering::Less => return, - cmp::Ordering::Greater | cmp::Ordering::Equal => { - cached_excerpt_hints.version = query.cache_version; - } - } - cached_excerpt_hints - .hints - .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - editor.inlay_hint_cache.version += 1; - - let mut splice = InlaySplice { - to_remove: new_update.remove_from_visible, - to_insert: Vec::new(), - }; - - for new_hint in new_update.add_to_cache { - let new_hint_position = multi_buffer_snapshot - .anchor_in_excerpt(query.excerpt_id, new_hint.position); - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) - { - splice.to_insert.push(Inlay::hint( - new_inlay_id, - new_hint_position, - &new_hint, - )); - } - - cached_excerpt_hints - .hints - .push((InlayId::Hint(new_inlay_id), new_hint)); - } - - cached_excerpt_hints - .hints - .sort_by(|(_, hint_a), (_, hint_b)| { - hint_a.position.cmp(&hint_b.position, &buffer_snapshot) - }); - drop(cached_excerpt_hints); - - if query.invalidate.should_invalidate() { - let mut outdated_excerpt_caches = HashSet::default(); - for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - if excerpt_hints.buffer_id == query.buffer_id - && excerpt_id != &query.excerpt_id - && buffer_snapshot - .version() - .changed_since(&excerpt_hints.buffer_version) - { - outdated_excerpt_caches.insert(*excerpt_id); - splice - .to_remove - .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); - } - } - editor - .inlay_hint_cache - .hints - .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); - } - - let InlaySplice { - to_remove, - to_insert, - } = splice; - if !to_remove.is_empty() || !to_insert.is_empty() { - editor.splice_inlay_hints(to_remove, to_insert, cx) - } - } - }) - .ok(); - - Ok(update_happened) + if let Some(new_update) = new_update { + editor + .update(&mut cx, |editor, cx| { + apply_hint_update( + editor, + new_update, + query, + buffer_snapshot, + multi_buffer_snapshot, + cx, + ); + }) + .ok(); + } + Ok(()) } fn calculate_hint_updates( @@ -793,19 +659,6 @@ fn calculate_hint_updates( visible_hints .iter() .filter(|hint| hint.position.excerpt_id == query.excerpt_id) - .filter(|hint| { - contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot) - }) - .filter(|hint| { - fetch_range - .start - .cmp(&hint.position.text_anchor, buffer_snapshot) - .is_le() - && fetch_range - .end - .cmp(&hint.position.text_anchor, buffer_snapshot) - .is_ge() - }) .map(|inlay_hint| inlay_hint.id) .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), ); @@ -819,16 +672,6 @@ fn calculate_hint_updates( .filter(|(cached_inlay_id, _)| { !excerpt_hints_to_persist.contains_key(cached_inlay_id) }) - .filter(|(_, cached_hint)| { - fetch_range - .start - .cmp(&cached_hint.position, buffer_snapshot) - .is_le() - && fetch_range - .end - .cmp(&cached_hint.position, buffer_snapshot) - .is_ge() - }) .map(|(cached_inlay_id, _)| *cached_inlay_id), ); } @@ -855,6 +698,113 @@ fn contains_position( && range.end.cmp(&position, buffer_snapshot).is_ge() } +fn apply_hint_update( + editor: &mut Editor, + new_update: ExcerptHintsUpdate, + query: ExcerptQuery, + buffer_snapshot: BufferSnapshot, + multi_buffer_snapshot: MultiBufferSnapshot, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let cached_excerpt_hints = editor + .inlay_hint_cache + .hints + .entry(new_update.excerpt_id) + .or_insert_with(|| { + Arc::new(RwLock::new(CachedExcerptHints { + version: query.cache_version, + buffer_version: buffer_snapshot.version().clone(), + buffer_id: query.buffer_id, + hints: Vec::new(), + })) + }); + let mut cached_excerpt_hints = cached_excerpt_hints.write(); + match query.cache_version.cmp(&cached_excerpt_hints.version) { + cmp::Ordering::Less => return, + cmp::Ordering::Greater | cmp::Ordering::Equal => { + cached_excerpt_hints.version = query.cache_version; + } + } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); + cached_excerpt_hints + .hints + .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); + let mut splice = InlaySplice { + to_remove: new_update.remove_from_visible, + to_insert: Vec::new(), + }; + for new_hint in new_update.add_to_cache { + let cached_hints = &mut cached_excerpt_hints.hints; + let insert_position = match cached_hints + .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot)) + { + Ok(i) => { + if cached_hints[i].1.text() == new_hint.text() { + None + } else { + Some(i) + } + } + Err(i) => Some(i), + }; + + if let Some(insert_position) = insert_position { + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + let new_hint_position = + multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position); + splice + .to_insert + .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); + } + cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); + cached_inlays_changed = true; + } + } + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); + drop(cached_excerpt_hints); + + if query.invalidate.should_invalidate() { + let mut outdated_excerpt_caches = HashSet::default(); + for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + if excerpt_hints.buffer_id == query.buffer_id + && excerpt_id != &query.excerpt_id + && buffer_snapshot + .version() + .changed_since(&excerpt_hints.buffer_version) + { + outdated_excerpt_caches.insert(*excerpt_id); + splice + .to_remove + .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); + } + } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + editor + .inlay_hint_cache + .hints + .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); + } + + let InlaySplice { + to_remove, + to_insert, + } = splice; + let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); + if cached_inlays_changed || displayed_inlays_changed { + editor.inlay_hint_cache.version += 1; + } + if displayed_inlays_changed { + editor.splice_inlay_hints(to_remove, to_insert, cx) + } +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -866,6 +816,7 @@ mod tests { }; use futures::StreamExt; use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; + use itertools::Itertools; use language::{ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, }; @@ -873,7 +824,7 @@ mod tests { use parking_lot::Mutex; use project::{FakeFs, Project}; use settings::SettingsStore; - use text::Point; + use text::{Point, ToPoint}; use workspace::Workspace; use crate::editor_tests::update_test_language_settings; @@ -1879,7 +1830,7 @@ mod tests { task_lsp_request_ranges.lock().push(params.range); let query_start = params.range.start; - let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; Ok(Some(vec![lsp::InlayHint { position: query_start, label: lsp::InlayHintLabel::String(i.to_string()), @@ -1894,18 +1845,44 @@ mod tests { }) .next() .await; + fn editor_visible_range( + editor: &ViewHandle, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); + assert_eq!( + ranges.len(), + 1, + "Single buffer should produce a single excerpt with visible range" + ); + let (_, (excerpt_buffer, _, excerpt_visible_range)) = + ranges.into_iter().next().unwrap(); + excerpt_buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let start = buffer + .anchor_before(excerpt_visible_range.start) + .to_point(&snapshot); + let end = buffer + .anchor_after(excerpt_visible_range.end) + .to_point(&snapshot); + start..end + }) + } + + let initial_visible_range = editor_visible_range(&editor, cx); + let expected_initial_query_range_end = + lsp::Position::new(initial_visible_range.end.row * 2, 1); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|range| range.start); - assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); - assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); - assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line"); - assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent"); + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + assert_eq!(ranges.len(), 1, + "When scroll is at the edge of a big document, double of its visible part range should be queried for hints in one single big request, but got: {ranges:?}"); + let query_range = &ranges[0]; + assert_eq!(query_range.start, lsp::Position::new(0, 0), "Should query initially from the beginning of the document"); + assert_eq!(query_range.end, expected_initial_query_range_end, "Should query initially for double lines of the visible part of the document"); - assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2, - "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); - let expected_layers = vec!["1".to_string(), "2".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 1); + let expected_layers = vec!["1".to_string()]; assert_eq!( expected_layers, cached_hint_labels(editor), @@ -1913,37 +1890,114 @@ mod tests { ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!( - editor.inlay_hint_cache().version, 2, - "Both LSP queries should've bumped the cache version" + editor.inlay_hint_cache().version, 1, + "LSP queries should've bumped the cache version" ); }); editor.update(cx, |editor, cx| { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); editor.scroll_screen(&ScrollAmount::Page(1.0), cx); - editor.change_selections(None, cx, |s| s.select_ranges([600..600])); - editor.handle_input("++++more text++++", cx); }); + let visible_range_after_scrolls = editor_visible_range(&editor, cx); + let visible_line_count = + editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); + cx.foreground().run_until_parked(); + let selection_in_cached_range = editor.update(cx, |editor, cx| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 2, + "Should query 2 ranges after both scrolls, but got: {ranges:?}" + ); + let first_scroll = &ranges[0]; + let second_scroll = &ranges[1]; + assert_eq!( + first_scroll.end, second_scroll.start, + "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" + ); + assert_eq!( + first_scroll.start, expected_initial_query_range_end, + "First scroll should start the query right after the end of the original scroll", + ); + assert_eq!( + second_scroll.end, + lsp::Position::new( + visible_range_after_scrolls.end.row + + visible_line_count.ceil() as u32, + 0 + ), + "Second scroll should query one more screen down after the end of the visible range" + ); + + assert_eq!( + lsp_request_count.load(Ordering::Acquire), + 3, + "Should query for hints after every scroll" + ); + let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should have hints from the new LSP response after the edit" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!( + editor.inlay_hint_cache().version, + 3, + "Should update the cache for every LSP response with hints added" + ); + + let mut selection_in_cached_range = visible_range_after_scrolls.end; + selection_in_cached_range.row -= visible_line_count.ceil() as u32; + selection_in_cached_range + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |_, _| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 3); + }); + + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|range| range.start); - assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints"); - assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); - assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end"); - assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning"); - assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning"); - assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line"); - assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent"); + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + assert_eq!(ranges.len(), 1, + "On edit, should scroll to selection and query a range around it. Instead, got query ranges {ranges:?}"); + let query_range = &ranges[0]; + assert!(query_range.start.line < selection_in_cached_range.row, + "Hints should be queried with the selected range after the query range start"); + assert!(query_range.end.line > selection_in_cached_range.row, + "Hints should be queried with the selected range before the query range end"); + assert!(query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen before"); + assert!(query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + "Hints query range should contain one more screen after"); - assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5, - "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints"); - let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4, "Should query for hints once after the edit"); + let expected_layers = vec!["4".to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor), - "Should have hints from the new LSP response after edit"); + "Should have hints from the new LSP response after the edit"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 4, "Should update the cache for every LSP response with hints added"); }); } @@ -2177,7 +2231,7 @@ mod tests { s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) }); editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) }); }); cx.foreground().run_until_parked(); @@ -2283,8 +2337,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, - last_scroll_update_version + expected_layers.len() + 1, - "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal" + last_scroll_update_version + expected_layers.len(), + "Due to every excerpt having one hint, cache should update per new excerpt received" ); }); } @@ -2488,8 +2542,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 3, - "Excerpt removal should trigger cache update" + 2, + "Excerpt removal should trigger a cache update" ); }); @@ -2516,12 +2570,119 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 4, - "Settings change should trigger cache update" + 3, + "Settings change should trigger a cache update" ); }); } + #[gpui::test] + async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let query_start = params.range.start; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; + Ok(Some(vec![lsp::InlayHint { + position: query_start, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_layers = vec!["1".to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + } + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { cx.foreground().forbid_parking(); diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 1bd37da52f..f70436abeb 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -13,6 +13,13 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { map.clip_point(point, Bias::Left) } +pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + if point.column() > 0 { + *point.column_mut() -= 1; + } + map.clip_point(point, Bias::Left) +} + pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { let max_column = map.line_len(point.row()); if point.column() < max_column { @@ -24,6 +31,11 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { map.clip_point(point, Bias::Right) } +pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { + *point.column_mut() += 1; + map.clip_point(point, Bias::Right) +} + pub fn up( map: &DisplaySnapshot, start: DisplayPoint, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 31af03f768..8417c411f2 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1565,6 +1565,25 @@ impl MultiBuffer { cx.add_model(|cx| Self::singleton(buffer, cx)) } + pub fn build_multi( + excerpts: [(&str, Vec>); COUNT], + cx: &mut gpui::AppContext, + ) -> ModelHandle { + let multi = cx.add_model(|_| Self::new(0)); + for (text, ranges) in excerpts { + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { + context: range, + primary: None, + }); + multi.update(cx, |multi, cx| { + multi.push_excerpts(buffer, excerpt_ranges, cx) + }); + } + + multi + } + pub fn build_from_buffer( buffer: ModelHandle, cx: &mut gpui::AppContext, diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index d595337428..1f3adaf477 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -13,7 +13,7 @@ use gpui::{ }; use language::{Bias, Point}; use util::ResultExt; -use workspace::{item::Item, WorkspaceId}; +use workspace::WorkspaceId; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, @@ -333,9 +333,7 @@ impl Editor { cx, ); - if !self.is_singleton(cx) { - self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); - } + self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index d197f57fa5..ad2a40b60c 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -35,7 +35,7 @@ impl View for DeployFeedbackButton { let theme = theme::current(cx).clone(); Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = &theme .workspace .status_bar diff --git a/crates/feedback/src/feedback_info_text.rs b/crates/feedback/src/feedback_info_text.rs index 6c55b7a713..91ff22e904 100644 --- a/crates/feedback/src/feedback_info_text.rs +++ b/crates/feedback/src/feedback_info_text.rs @@ -41,7 +41,7 @@ impl View for FeedbackInfoText { .aligned(), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let contained_text = if state.hovered() { &theme.feedback.link_text_hover } else { diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs index 2133296e25..df59cf143f 100644 --- a/crates/feedback/src/submit_feedback_button.rs +++ b/crates/feedback/src/submit_feedback_button.rs @@ -52,7 +52,7 @@ impl View for SubmitFeedbackButton { .map_or(true, |i| i.read(cx).allow_submission); enum SubmitFeedbackButton {} - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let text; let style = if allow_submission { text = "Submit as Markdown"; diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs new file mode 100644 index 0000000000..cf695ea834 --- /dev/null +++ b/crates/gpui/examples/components.rs @@ -0,0 +1,237 @@ +use button_component::Button; + +use gpui::{ + color::Color, + elements::{Component, ContainerStyle, Flex, Label, ParentElement}, + fonts::{self, TextStyle}, + platform::WindowOptions, + AnyElement, App, Element, Entity, View, ViewContext, +}; +use log::LevelFilter; +use pathfinder_geometry::vector::vec2f; +use simplelog::SimpleLogger; +use theme::Toggleable; +use toggleable_button::ToggleableButton; + +// cargo run -p gpui --example components + +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + App::new(()).unwrap().run(|cx| { + cx.platform().activate(true); + cx.add_window(WindowOptions::with_bounds(vec2f(300., 200.)), |_| { + TestView { + count: 0, + is_doubling: false, + } + }); + }); +} + +pub struct TestView { + count: usize, + is_doubling: bool, +} + +impl TestView { + fn increase_count(&mut self) { + if self.is_doubling { + self.count *= 2; + } else { + self.count += 1; + } + } +} + +impl Entity for TestView { + type Event = (); +} + +type ButtonStyle = ContainerStyle; + +impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement { + fonts::with_font_cache(cx.font_cache.to_owned(), || { + Flex::column() + .with_child(Label::new( + format!("Count: {}", self.count), + TextStyle::for_color(Color::red()), + )) + .with_child( + Button::new(move |_, v: &mut Self, cx| { + v.increase_count(); + cx.notify(); + }) + .with_text( + "Hello from a counting BUTTON", + TextStyle::for_color(Color::blue()), + ) + .with_style(ButtonStyle::fill(Color::yellow())) + .into_element(), + ) + .with_child( + ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { + v.is_doubling = !v.is_doubling; + cx.notify(); + }) + .with_text("Double the count?", TextStyle::for_color(Color::black())) + .with_style(Toggleable { + inactive: ButtonStyle::fill(Color::red()), + active: ButtonStyle::fill(Color::green()), + }) + .into_element(), + ) + .expanded() + .contained() + .with_background_color(Color::white()) + .into_any() + }) + } +} + +mod theme { + pub struct Toggleable { + pub inactive: T, + pub active: T, + } + + impl Toggleable { + pub fn style_for(&self, active: bool) -> &T { + if active { + &self.active + } else { + &self.inactive + } + } + } +} + +// Component creation: +mod toggleable_button { + use gpui::{ + elements::{Component, ContainerStyle, LabelStyle}, + scene::MouseClick, + EventContext, View, + }; + + use crate::{button_component::Button, theme::Toggleable}; + + pub struct ToggleableButton { + active: bool, + style: Option>, + button: Button, + } + + impl ToggleableButton { + pub fn new(active: bool, on_click: F) -> Self + where + F: Fn(MouseClick, &mut V, &mut EventContext) + 'static, + { + Self { + active, + button: Button::new(on_click), + style: None, + } + } + + pub fn with_text(self, text: &str, style: impl Into) -> ToggleableButton { + ToggleableButton { + active: self.active, + style: self.style, + button: self.button.with_text(text, style), + } + } + + pub fn with_style(self, style: Toggleable) -> ToggleableButton { + ToggleableButton { + active: self.active, + style: Some(style), + button: self.button, + } + } + } + + impl Component for ToggleableButton { + fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + let button = if let Some(style) = self.style { + self.button.with_style(*style.style_for(self.active)) + } else { + self.button + }; + button.render(v, cx) + } + } +} + +mod button_component { + + use gpui::{ + elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler}, + platform::MouseButton, + scene::MouseClick, + AnyElement, Element, EventContext, TypeTag, View, ViewContext, + }; + + type ClickHandler = Box)>; + + pub struct Button { + click_handler: ClickHandler, + tag: TypeTag, + contents: Option>, + style: Option, + } + + impl Button { + pub fn new) + 'static>(handler: F) -> Self { + Self { + click_handler: Box::new(handler), + tag: TypeTag::new::(), + style: None, + contents: None, + } + } + + pub fn with_text(mut self, text: &str, style: impl Into) -> Self { + self.contents = Some(Label::new(text.to_string(), style).into_any()); + self + } + + pub fn _with_contents>(mut self, contents: E) -> Self { + self.contents = Some(contents.into_any()); + self + } + + pub fn with_style(mut self, style: ContainerStyle) -> Self { + self.style = Some(style); + self + } + } + + impl Component for Button { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + let click_handler = self.click_handler; + + let result = MouseEventHandler::new_dynamic(self.tag, 0, cx, |_, _| { + self.contents + .unwrap_or_else(|| gpui::elements::Empty::new().into_any()) + }) + .on_click(MouseButton::Left, move |click, v, cx| { + click_handler(click, v, cx); + }) + .contained(); + + let result = if let Some(style) = self.style { + result.with_style(style) + } else { + result + }; + + result.into_any() + } + } +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2a9d9f4768..8e6d43a45d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3280,7 +3280,11 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } pub fn mouse_state(&self, region_id: usize) -> MouseState { - let region_id = MouseRegionId::new::(self.view_id, region_id); + self.mouse_state_dynamic(TypeTag::new::(), region_id) + } + + pub fn mouse_state_dynamic(&self, tag: TypeTag, region_id: usize) -> MouseState { + let region_id = MouseRegionId::new(tag, self.view_id, region_id); MouseState { hovered: self.window.hovered_region_ids.contains(®ion_id), clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region { @@ -3321,6 +3325,36 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } } +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct TypeTag { + tag: TypeId, + #[cfg(debug_assertions)] + tag_type_name: &'static str, +} + +impl TypeTag { + pub fn new() -> Self { + Self { + tag: TypeId::of::(), + #[cfg(debug_assertions)] + tag_type_name: std::any::type_name::(), + } + } + + pub fn dynamic(tag: TypeId, #[cfg(debug_assertions)] type_name: &'static str) -> Self { + Self { + tag, + #[cfg(debug_assertions)] + tag_type_name: type_name, + } + } + + #[cfg(debug_assertions)] + pub(crate) fn type_name(&self) -> &'static str { + self.tag_type_name + } +} + impl BorrowAppContext for ViewContext<'_, '_, V> { fn read_with T>(&self, f: F) -> T { BorrowAppContext::read_with(&*self.window_context, f) @@ -5171,7 +5205,7 @@ mod tests { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { enum Handler {} let mouse_down_count = self.mouse_down_count.clone(); - MouseEventHandler::::new(0, cx, |_, _| Empty::new()) + MouseEventHandler::new::(0, cx, |_, _| Empty::new()) .on_down(MouseButton::Left, move |_, _, _| { mouse_down_count.fetch_add(1, SeqCst); }) diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 56a712802b..35ecf0545a 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -1,6 +1,7 @@ mod align; mod canvas; mod clipped; +mod component; mod constrained_box; mod container; mod empty; @@ -21,9 +22,9 @@ mod tooltip; mod uniform_list; pub use self::{ - align::*, canvas::*, constrained_box::*, container::*, empty::*, flex::*, hook::*, image::*, - keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, resizable::*, - stack::*, svg::*, text::*, tooltip::*, uniform_list::*, + align::*, canvas::*, component::*, constrained_box::*, container::*, empty::*, flex::*, + hook::*, image::*, keystroke_label::*, label::*, list::*, mouse_event_handler::*, overlay::*, + resizable::*, stack::*, svg::*, text::*, tooltip::*, uniform_list::*, }; pub use crate::window::ChildView; @@ -193,11 +194,11 @@ pub trait Element: 'static { Resizable::new(self.into_any(), side, size, on_resize) } - fn mouse(self, region_id: usize) -> MouseEventHandler + fn mouse(self, region_id: usize) -> MouseEventHandler where Self: Sized, { - MouseEventHandler::for_child(self.into_any(), region_id) + MouseEventHandler::for_child::(self.into_any(), region_id) } } diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs new file mode 100644 index 0000000000..1c4359e2c3 --- /dev/null +++ b/crates/gpui/src/elements/component.rs @@ -0,0 +1,87 @@ +use std::marker::PhantomData; + +use pathfinder_geometry::{rect::RectF, vector::Vector2F}; + +use crate::{ + AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, +}; + +pub trait Component { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + + fn into_element(self) -> ComponentAdapter + where + Self: Sized, + { + ComponentAdapter::new(self) + } +} + +pub struct ComponentAdapter { + component: Option, + phantom: PhantomData, +} + +impl ComponentAdapter { + pub fn new(e: E) -> Self { + Self { + component: Some(e), + phantom: PhantomData, + } + } +} + +impl + 'static> Element for ComponentAdapter { + type LayoutState = AnyElement; + + type PaintState = (); + + fn layout( + &mut self, + constraint: SizeConstraint, + view: &mut V, + cx: &mut LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let component = self.component.take().unwrap(); + let mut element = component.render(view, cx.view_context()); + let constraint = element.layout(constraint, view, cx); + (constraint, element) + } + + fn paint( + &mut self, + scene: &mut SceneBuilder, + bounds: RectF, + visible_bounds: RectF, + layout: &mut Self::LayoutState, + view: &mut V, + cx: &mut PaintContext, + ) -> Self::PaintState { + layout.paint(scene, bounds.origin(), visible_bounds, view, cx) + } + + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _: RectF, + _: RectF, + element: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &ViewContext, + ) -> Option { + element.rect_for_text_range(range_utf16, view, cx) + } + + fn debug( + &self, + _: RectF, + element: &Self::LayoutState, + _: &Self::PaintState, + view: &V, + cx: &ViewContext, + ) -> serde_json::Value { + element.debug(view, cx) + } +} diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 698100ab29..bb1366b4e7 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -38,6 +38,15 @@ pub struct ContainerStyle { pub cursor: Option, } +impl ContainerStyle { + pub fn fill(color: Color) -> Self { + Self { + background_color: Some(color), + ..Default::default() + } + } +} + pub struct Container { child: AnyElement, style: ContainerStyle, diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 6005277f73..2ed0f1720f 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -11,12 +11,12 @@ use crate::{ MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, }, AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext, - SceneBuilder, SizeConstraint, View, ViewContext, + SceneBuilder, SizeConstraint, TypeTag, View, ViewContext, }; use serde_json::json; -use std::{marker::PhantomData, ops::Range}; +use std::ops::Range; -pub struct MouseEventHandler { +pub struct MouseEventHandler { child: AnyElement, region_id: usize, cursor_style: Option, @@ -26,13 +26,13 @@ pub struct MouseEventHandler { notify_on_click: bool, above: bool, padding: Padding, - _tag: PhantomData, + tag: TypeTag, } /// Element which provides a render_child callback with a MouseState and paints a mouse /// region under (or above) it for easy mouse event handling. -impl MouseEventHandler { - pub fn for_child(child: impl Element, region_id: usize) -> Self { +impl MouseEventHandler { + pub fn for_child(child: impl Element, region_id: usize) -> Self { Self { child: child.into_any(), region_id, @@ -43,16 +43,19 @@ impl MouseEventHandler { hoverable: false, above: false, padding: Default::default(), - _tag: PhantomData, + tag: TypeTag::new::(), } } - pub fn new(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self + pub fn new( + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> E, + ) -> Self where E: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> E, { - let mut mouse_state = cx.mouse_state::(region_id); + let mut mouse_state = cx.mouse_state_dynamic(TypeTag::new::(), region_id); let child = render_child(&mut mouse_state, cx).into_any(); let notify_on_hover = mouse_state.accessed_hovered(); let notify_on_click = mouse_state.accessed_clicked(); @@ -66,19 +69,46 @@ impl MouseEventHandler { hoverable: true, above: false, padding: Default::default(), - _tag: PhantomData, + tag: TypeTag::new::(), + } + } + + pub fn new_dynamic( + tag: TypeTag, + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> AnyElement, + ) -> Self { + let mut mouse_state = cx.mouse_state_dynamic(tag, region_id); + let child = render_child(&mut mouse_state, cx); + let notify_on_hover = mouse_state.accessed_hovered(); + let notify_on_click = mouse_state.accessed_clicked(); + Self { + child, + region_id, + cursor_style: None, + handlers: Default::default(), + notify_on_hover, + notify_on_click, + hoverable: true, + above: false, + padding: Default::default(), + tag, } } /// Modifies the MouseEventHandler to render the MouseRegion above the child element. Useful /// for drag and drop handling and similar events which should be captured before the child /// gets the opportunity - pub fn above(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self + pub fn above( + region_id: usize, + cx: &mut ViewContext, + render_child: impl FnOnce(&mut MouseState, &mut ViewContext) -> D, + ) -> Self where D: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> D, { - let mut handler = Self::new(region_id, cx, render_child); + let mut handler = Self::new::(region_id, cx, render_child); handler.above = true; handler } @@ -223,7 +253,8 @@ impl MouseEventHandler { }); } scene.push_mouse_region( - MouseRegion::from_handlers::( + MouseRegion::from_handlers( + self.tag, cx.view_id(), self.region_id, hit_bounds, @@ -236,7 +267,7 @@ impl MouseEventHandler { } } -impl Element for MouseEventHandler { +impl Element for MouseEventHandler { type LayoutState = (); type PaintState = (); diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 14f3809e67..0ba0110303 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -95,7 +95,7 @@ impl Tooltip { } else { None }; - let child = MouseEventHandler::, _>::new(id, cx, |_, _| child) + let child = MouseEventHandler::new::, _>(id, cx, |_, _| child) .on_hover(move |e, _, cx| { let position = e.position; if e.started { diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index b003042866..8858e1a316 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -72,6 +72,13 @@ pub struct TextStyle { } impl TextStyle { + pub fn for_color(color: Color) -> Self { + Self { + color, + ..Default::default() + } + } + pub fn refine(self, refinement: TextStyleRefinement) -> TextStyle { TextStyle { color: refinement.color.unwrap_or(self.color), diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 1d93a45fc7..9f6e303cb7 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -24,6 +24,7 @@ use crate::{ use anyhow::{anyhow, bail, Result}; use async_task::Runnable; pub use event::*; +use pathfinder_geometry::vector::vec2f; use postage::oneshot; use schemars::JsonSchema; use serde::Deserialize; @@ -180,6 +181,16 @@ pub struct WindowOptions<'a> { pub screen: Option>, } +impl<'a> WindowOptions<'a> { + pub fn with_bounds(bounds: Vector2F) -> Self { + Self { + bounds: WindowBounds::Fixed(RectF::new(vec2f(0., 0.), bounds)), + center: true, + ..Default::default() + } + } +} + #[derive(Debug)] pub struct TitlebarOptions<'a> { pub title: Option<&'a str>, diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index cf39ac782f..1632b494a3 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -1,13 +1,10 @@ -use crate::{platform::MouseButton, window::WindowContext, EventContext, View, ViewContext}; +use crate::{ + platform::MouseButton, window::WindowContext, EventContext, TypeTag, View, ViewContext, +}; use collections::HashMap; use pathfinder_geometry::rect::RectF; use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - fmt::Debug, - mem::Discriminant, - rc::Rc, -}; +use std::{any::Any, fmt::Debug, mem::Discriminant, rc::Rc}; use super::{ mouse_event::{ @@ -33,14 +30,27 @@ impl MouseRegion { /// should pass a different (consistent) region_id. If you have one big region that covers your /// whole component, just pass the view_id again. pub fn new(view_id: usize, region_id: usize, bounds: RectF) -> Self { - Self::from_handlers::(view_id, region_id, bounds, Default::default()) + Self::from_handlers( + TypeTag::new::(), + view_id, + region_id, + bounds, + Default::default(), + ) } pub fn handle_all(view_id: usize, region_id: usize, bounds: RectF) -> Self { - Self::from_handlers::(view_id, region_id, bounds, HandlerSet::capture_all()) + Self::from_handlers( + TypeTag::new::(), + view_id, + region_id, + bounds, + HandlerSet::capture_all(), + ) } - pub fn from_handlers( + pub fn from_handlers( + tag: TypeTag, view_id: usize, region_id: usize, bounds: RectF, @@ -49,10 +59,8 @@ impl MouseRegion { Self { id: MouseRegionId { view_id, - tag: TypeId::of::(), + tag, region_id, - #[cfg(debug_assertions)] - tag_type_name: std::any::type_name::(), }, bounds, handlers, @@ -180,20 +188,16 @@ impl MouseRegion { #[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] pub struct MouseRegionId { view_id: usize, - tag: TypeId, + tag: TypeTag, region_id: usize, - #[cfg(debug_assertions)] - tag_type_name: &'static str, } impl MouseRegionId { - pub(crate) fn new(view_id: usize, region_id: usize) -> Self { + pub(crate) fn new(tag: TypeTag, view_id: usize, region_id: usize) -> Self { MouseRegionId { view_id, region_id, - tag: TypeId::of::(), - #[cfg(debug_assertions)] - tag_type_name: std::any::type_name::(), + tag, } } @@ -203,7 +207,7 @@ impl MouseRegionId { #[cfg(debug_assertions)] pub fn tag_type_name(&self) -> &'static str { - self.tag_type_name + self.tag.type_name() } } diff --git a/crates/gpui/src/views/select.rs b/crates/gpui/src/views/select.rs index f3be9de3ec..f76fab738e 100644 --- a/crates/gpui/src/views/select.rs +++ b/crates/gpui/src/views/select.rs @@ -106,7 +106,7 @@ impl View for Select { Default::default() }; let mut result = Flex::column().with_child( - MouseEventHandler::::new(self.handle.id(), cx, |mouse_state, cx| { + MouseEventHandler::new::(self.handle.id(), cx, |mouse_state, cx| { (self.render_item)( self.selected_item_ix, ItemType::Header, @@ -130,7 +130,7 @@ impl View for Select { let selected_item_ix = this.selected_item_ix; range.end = range.end.min(this.item_count); items.extend(range.map(|ix| { - MouseEventHandler::::new(ix, cx, |mouse_state, cx| { + MouseEventHandler::new::(ix, cx, |mouse_state, cx| { (this.render_item)( ix, if ix == selected_item_ix { diff --git a/crates/gpui_macros/tests/test.rs b/crates/gpui/tests/test.rs similarity index 100% rename from crates/gpui_macros/tests/test.rs rename to crates/gpui/tests/test.rs diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml index 9ff340299b..9d1d232c95 100644 --- a/crates/gpui_macros/Cargo.toml +++ b/crates/gpui_macros/Cargo.toml @@ -13,6 +13,3 @@ doctest = false syn = "1.0" quote = "1.0" proc-macro2 = "1.0" - -[dev-dependencies] -gpui = { path = "../gpui" } diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index b97417580f..5ffcb13fba 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -53,7 +53,7 @@ impl View for ActiveBufferLanguage { "Unknown".to_string() }; - MouseEventHandler::::new(0, cx, |state, cx| { + MouseEventHandler::new::(0, cx, |state, cx| { let theme = &theme::current(cx).workspace.status_bar; let style = theme.active_language.style_for(state); Label::new(active_language_text, style.text.clone()) diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 0dc594a13f..cc2bf37d4a 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -573,7 +573,7 @@ impl View for LspLogToolbarItemView { .with_children(if self.menu_open { Some( Overlay::new( - MouseEventHandler::::new(0, cx, move |_, cx| { + MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() .with_children(menu_rows.into_iter().map(|row| { Self::render_language_server_menu_item( @@ -672,7 +672,7 @@ impl LspLogToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ToggleMenu {} - MouseEventHandler::::new(0, cx, move |state, cx| { + MouseEventHandler::new::(0, cx, move |state, cx| { let label: Cow = current_server .and_then(|row| { let worktree = row.worktree.read(cx); @@ -728,7 +728,7 @@ impl LspLogToolbarItemView { .with_height(theme.toolbar_dropdown_menu.row_height) }) .with_child( - MouseEventHandler::::new(id.0, cx, move |state, _| { + MouseEventHandler::new::(id.0, cx, move |state, _| { let style = theme .toolbar_dropdown_menu .item @@ -746,7 +746,7 @@ impl LspLogToolbarItemView { }), ) .with_child( - MouseEventHandler::::new(id.0, cx, move |state, cx| { + MouseEventHandler::new::(id.0, cx, move |state, cx| { let style = theme .toolbar_dropdown_menu .item diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 3e6727bbf4..60788d034e 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -389,7 +389,7 @@ impl View for SyntaxTreeView { { let layer = layer.clone(); let theme = editor_theme.clone(); - return MouseEventHandler::::new(0, cx, move |state, cx| { + return MouseEventHandler::new::(0, cx, move |state, cx| { let list_hovered = state.hovered(); UniformList::new( self.list_state.clone(), @@ -505,7 +505,7 @@ impl SyntaxTreeToolbarItemView { .with_child(Self::render_header(&theme, &active_layer, cx)) .with_children(self.menu_open.then(|| { Overlay::new( - MouseEventHandler::::new(0, cx, move |_, cx| { + MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() .with_children(active_buffer.syntax_layers().enumerate().map( |(ix, layer)| { @@ -564,7 +564,7 @@ impl SyntaxTreeToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ToggleMenu {} - MouseEventHandler::::new(0, cx, move |state, _| { + MouseEventHandler::new::(0, cx, move |state, _| { let style = theme.toolbar_dropdown_menu.header.style_for(state); Flex::row() .with_child( @@ -596,7 +596,7 @@ impl SyntaxTreeToolbarItemView { cx: &mut ViewContext, ) -> impl Element { enum ActivateLayer {} - MouseEventHandler::::new(layer_ix, cx, move |state, _| { + MouseEventHandler::new::(layer_ix, cx, move |state, _| { let is_selected = layer.node() == active_layer.node(); let style = theme .toolbar_dropdown_menu diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 78c858a90c..e0ae64d806 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,7 +434,9 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: None, + resolve_support: Some(InlayHintResolveClientCapabilities { + properties: vec!["textEdits".to_string(), "tooltip".to_string()], + }), dynamic_registration: Some(false), }), ..Default::default() diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 6efa33e961..a3b8672f9f 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -112,7 +112,7 @@ impl View for Picker { let selected_ix = this.delegate.selected_index(); range.end = cmp::min(range.end, this.delegate.match_count()); items.extend(range.map(move |ix| { - MouseEventHandler::::new(ix, cx, |state, cx| { + MouseEventHandler::new::(ix, cx, |state, cx| { this.delegate.render_match(ix, state, ix == selected_ix, cx) }) // Capture mouse events diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f1..a8692257d8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1954,7 +1954,7 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - cx: &mut AppContext, + _: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response @@ -1963,51 +1963,17 @@ impl LspCommand for InlayHints { position: Some(language::proto::serialize_anchor(&response_hint.position)), padding_left: response_hint.padding_left, padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) - } - }), - }), kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } + // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution. + tooltip: None, + // Similarly, do not pass label parts to clients: host can return a detailed list during resolution. + label: Some(proto::InlayHintLabel { + label: Some(proto::inlay_hint_label::Label::Value( + match response_hint.label { + InlayHintLabel::String(s) => s, + InlayHintLabel::LabelParts(_) => response_hint.text(), + }, + )), }), }) .collect(), diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index f7582f1764..4acc539263 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1407,7 +1407,7 @@ impl ProjectPanel { let show_editor = details.is_editing && !details.is_processing; - MouseEventHandler::::new(entry_id.to_usize(), cx, |state, cx| { + MouseEventHandler::new::(entry_id.to_usize(), cx, |state, cx| { let mut style = entry_style .in_state(details.is_selected) .style_for(state) @@ -1519,7 +1519,7 @@ impl View for ProjectPanel { if has_worktree { Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { UniformList::new( self.list.clone(), self.visible_entries @@ -1563,7 +1563,7 @@ impl View for ProjectPanel { } else { Flex::column() .with_child( - MouseEventHandler::::new(2, cx, { + MouseEventHandler::new::(2, cx, { let button_style = theme.open_project_button.clone(); let context_menu_item_style = theme::current(cx).context_menu.item.clone(); move |state, cx| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 32e6c93f4a..daf92151fc 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -494,7 +494,7 @@ impl BufferSearchBar { CursorStyle::default() }; enum ActionButton {} - MouseEventHandler::::new(action_type_id, cx, |state, cx| { + MouseEventHandler::new::(action_type_id, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -519,6 +519,7 @@ impl BufferSearchBar { ) .into_any() } + pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { assert_ne!( mode, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index fb0e71b1ed..d0a1aa95c1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -406,7 +406,7 @@ impl View for ProjectSearchView { editor.set_placeholder_text(new_placeholder_text, cx); }); - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Flex::column() .with_child(Flex::column().contained().flex(1., true)) .with_child( diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 47892c0ca6..88d4675102 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -23,7 +23,7 @@ pub(super) fn render_close_button( let tooltip_style = theme::current(cx).tooltip.clone(); enum CloseButton {} - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) @@ -68,7 +68,7 @@ pub(super) fn render_nav_button( CursorStyle::default() }; enum NavButton {} - MouseEventHandler::::new(direction as usize, cx, |state, cx| { + MouseEventHandler::new::(direction as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -117,7 +117,7 @@ pub(crate) fn render_search_mode_button( ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); enum SearchModeButton {} - MouseEventHandler::::new(mode.region_id(), cx, |state, cx| { + MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { let theme = theme::current(cx); let mut style = theme .search @@ -177,7 +177,7 @@ pub(crate) fn render_option_button_icon( cx: &mut ViewContext, ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); - MouseEventHandler::::new(id, cx, |state, cx| { + MouseEventHandler::new::(id, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index f6cfe5ae30..3bae06a86d 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -987,6 +987,14 @@ impl Terminal { } } + pub fn select_all(&mut self) { + let term = self.term.lock(); + let start = Point::new(term.topmost_line(), Column(0)); + let end = Point::new(term.bottommost_line(), term.last_column()); + drop(term); + self.set_selection(Some((make_selection(&(start..=end)), end))); + } + fn set_selection(&mut self, selection: Option<(Selection, Point)>) { self.events .push_back(InternalEvent::SetSelection(selection)); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 970e0115df..b48597d901 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -80,6 +80,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(TerminalView::paste); cx.add_action(TerminalView::clear); cx.add_action(TerminalView::show_character_palette); + cx.add_action(TerminalView::select_all) } ///A terminal view, maintains the PTY's file handles and communicates with the terminal @@ -312,6 +313,11 @@ impl TerminalView { } } + fn select_all(&mut self, _: &editor::SelectAll, cx: &mut ViewContext) { + self.terminal.update(cx, |term, _| term.select_all()); + cx.notify(); + } + fn clear(&mut self, _: &Clear, cx: &mut ViewContext) { self.terminal.update(cx, |term, _| term.clear()); cx.notify(); diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index a16c3cb21e..81663ed6ca 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -34,7 +34,7 @@ pub fn checkbox( id: usize, cx: &mut ViewContext, change: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, V: View, @@ -43,7 +43,7 @@ where let label = Label::new(label, style.label.text.clone()) .contained() .with_style(style.label.container); - checkbox_with_label(label, style, checked, id, cx, change) + checkbox_with_label::(label, style, checked, id, cx, change) } pub fn checkbox_with_label( @@ -53,14 +53,14 @@ pub fn checkbox_with_label( id: usize, cx: &mut ViewContext, change: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, D: Element, V: View, F: 'static + Fn(&mut V, bool, &mut EventContext), { - MouseEventHandler::new(id, cx, |state, _| { + MouseEventHandler::new::(id, cx, |state, _| { let indicator = if checked { svg(&style.icon) } else { @@ -143,14 +143,14 @@ pub fn cta_button( style: &ButtonStyle, cx: &mut ViewContext, f: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, L: Into>, V: View, F: Fn(MouseClick, &mut V, &mut EventContext) + 'static, { - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Label::new(label, style.text.to_owned()) .aligned() @@ -205,7 +205,7 @@ where )) .with_child( // FIXME: Get a better tag type - MouseEventHandler::::new(999999, cx, |state, _cx| { + MouseEventHandler::new::(999999, cx, |state, _cx| { let style = style.close_icon.style_for(state); icon(style) }) diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 384b622469..9009c4e3d3 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -295,7 +295,7 @@ impl PickerDelegate for BranchListDelegate { let style = theme.picker.footer.clone(); enum BranchCreateButton {} Some( - Flex::row().with_child(MouseEventHandler::::new(0, cx, |state, _| { + Flex::row().with_child(MouseEventHandler::new::(0, cx, |state, _| { let style = style.style_for(state); Label::new("Create branch", style.label.clone()) .contained() diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 5d54a66723..48cae9f4ae 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -87,7 +87,7 @@ impl View for ModeIndicator { Mode::Normal => "-- NORMAL --", Mode::Insert => "-- INSERT --", Mode::Visual { line: false } => "-- VISUAL --", - Mode::Visual { line: true } => "VISUAL LINE ", + Mode::Visual { line: true } => "VISUAL LINE", }; Label::new(text, theme.vim_mode_indicator.text.clone()) .contained() diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index b8bd256d8a..acf9d46ad3 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -383,8 +383,7 @@ impl Motion { fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { for _ in 0..times { - *point.column_mut() = point.column().saturating_sub(1); - point = map.clip_point(point, Bias::Left); + point = movement::saturating_left(map, point); if point.column() == 0 { break; } @@ -425,9 +424,7 @@ fn up( pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { for _ in 0..times { - let mut new_point = point; - *new_point.column_mut() += 1; - let new_point = map.clip_point(new_point, Bias::Right); + let new_point = movement::saturating_right(map, point); if point == new_point { break; } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 79c990ffeb..5ac3e86165 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -3,7 +3,7 @@ mod change; mod delete; mod scroll; mod search; -mod substitute; +pub mod substitute; mod yank; use std::{borrow::Cow, sync::Arc}; diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index ef72baae31..d2429433fe 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -1,34 +1,45 @@ use gpui::WindowContext; use language::Point; -use crate::{motion::Motion, Mode, Vim}; +use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim}; pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { + let line_mode = vim.state.mode == Mode::Visual { line: true }; + vim.switch_mode(Mode::Insert, true, cx); vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); editor.transact(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { if selection.start == selection.end { Motion::Right.expand_selection(map, selection, count, true); } + if line_mode { + Motion::CurrentLine.expand_selection(map, selection, None, false); + if let Some((point, _)) = Motion::FirstNonWhitespace.move_point( + map, + selection.start, + selection.goal, + None, + ) { + selection.start = point; + } + } }) }); - let selections = editor.selections.all::(cx); - for selection in selections.into_iter().rev() { - editor.buffer().update(cx, |buffer, cx| { - buffer.edit([(selection.start..selection.end, "")], None, cx) - }) - } + copy_selections_content(editor, line_mode, cx); + let selections = editor.selections.all::(cx).into_iter(); + let edits = selections.map(|selection| (selection.start..selection.end, "")); + editor.edit(edits, cx); }); - editor.set_clip_at_line_ends(true, cx); }); - vim.switch_mode(Mode::Insert, true, cx) } #[cfg(test)] mod test { - use crate::{state::Mode, test::VimTestContext}; + use crate::{ + state::Mode, + test::{NeovimBackedTestContext, VimTestContext}, + }; use indoc::indoc; #[gpui::test] @@ -69,5 +80,86 @@ mod test { // should transactionally undo selection changes cx.simulate_keystrokes(["escape", "u"]); cx.assert_editor_state("ˇcàfé\n"); + + // it handles visual line mode + cx.set_state( + indoc! {" + alpha + beˇta + gamma"}, + Mode::Normal, + ); + cx.simulate_keystrokes(["shift-v", "s"]); + cx.assert_editor_state(indoc! {" + alpha + ˇ + gamma"}); + } + + #[gpui::test] + async fn test_visual_change(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("The quick ˇbrown").await; + cx.simulate_shared_keystrokes(["v", "w", "c"]).await; + cx.assert_shared_state("The quick ˇ").await; + + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await; + cx.assert_shared_state(indoc! {" + The ˇver + the lazy dog"}) + .await; + + let cases = cx.each_marked_position(indoc! {" + The ˇquick brown + fox jumps ˇover + the ˇlazy dog"}); + for initial_state in cases { + cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"]) + .await; + cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"]) + .await; + } + } + + #[gpui::test] + async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx) + .await + .binding(["shift-v", "c"]); + cx.assert(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + .await; + // Test pasting code copied on change + cx.simulate_shared_keystrokes(["escape", "j", "p"]).await; + cx.assert_state_matches().await; + + cx.assert_all(indoc! {" + The quick brown + fox juˇmps over + the laˇzy dog"}) + .await; + let mut cx = cx.binding(["shift-v", "j", "c"]); + cx.assert(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + .await; + // Test pasting code copied on delete + cx.simulate_shared_keystrokes(["escape", "j", "p"]).await; + cx.assert_state_matches().await; + + cx.assert_all(indoc! {" + The quick brown + fox juˇmps over + the laˇzy dog"}) + .await; } } diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 5388dac1a2..85e6eab692 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -369,7 +369,7 @@ fn surrounding_markers( start = Some(point) } else { *point.column_mut() += char.len_utf8() as u32; - start = Some(point); + start = Some(point) } break; } @@ -420,11 +420,38 @@ fn surrounding_markers( } } - if let (Some(start), Some(end)) = (start, end) { - Some(start..end) - } else { - None + let (Some(mut start), Some(mut end)) = (start, end) else { + return None; + }; + + if !around { + // if a block starts with a newline, move the start to after the newline. + let mut was_newline = false; + for (char, point) in map.chars_at(start) { + if was_newline { + start = point; + } else if char == '\n' { + was_newline = true; + continue; + } + break; + } + // if a block ends with a newline, then whitespace, then the delimeter, + // move the end to after the newline. + let mut new_end = end; + for (char, point) in map.reverse_chars_at(end) { + if char == '\n' { + end = new_end; + break; + } + if !char.is_whitespace() { + break; + } + new_end = point + } } + + Some(start..end) } #[cfg(test)] @@ -481,6 +508,12 @@ mod test { async fn test_visual_word_object(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state("The quick ˇbrown\nfox").await; + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state("The quick «bˇ»rown\nfox").await; + cx.simulate_shared_keystrokes(["i", "w"]).await; + cx.assert_shared_state("The quick «brownˇ»\nfox").await; + cx.assert_binding_matches_all(["v", "i", "w"], WORD_LOCATIONS) .await; cx.assert_binding_matches_all_exempted( @@ -675,6 +708,48 @@ mod test { } } + #[gpui::test] + async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "func empty(a string) bool { + if a == \"\" { + return true + } + ˇreturn false + }" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "{"]).await; + cx.assert_shared_state(indoc! {" + func empty(a string) bool { + « if a == \"\" { + return true + } + return false + ˇ»}"}) + .await; + cx.set_shared_state(indoc! { + "func empty(a string) bool { + if a == \"\" { + ˇreturn true + } + return false + }" + }) + .await; + cx.simulate_shared_keystrokes(["v", "i", "{"]).await; + cx.assert_shared_state(indoc! {" + func empty(a string) bool { + if a == \"\" { + « return true + ˇ» } + return false + }"}) + .await; + } + #[gpui::test] async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index eb52945ced..905bd5fd2a 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -12,6 +12,15 @@ pub enum Mode { Visual { line: bool }, } +impl Mode { + pub fn is_visual(&self) -> bool { + match self { + Mode::Normal | Mode::Insert => false, + Mode::Visual { .. } => true, + } + } +} + impl Default for Mode { fn default() -> Self { Self::Normal @@ -78,12 +87,11 @@ impl VimState { ) } - pub fn clip_at_line_end(&self) -> bool { - !matches!(self.mode, Mode::Insert | Mode::Visual { .. }) - } - - pub fn empty_selections_only(&self) -> bool { - !matches!(self.mode, Mode::Visual { .. }) + pub fn clip_at_line_ends(&self) -> bool { + match self.mode { + Mode::Insert | Mode::Visual { .. } => false, + Mode::Normal => true, + } } pub fn keymap_context_layer(&self) -> KeymapContext { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 474f2128fc..eb2e6e3a5f 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -141,7 +141,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { // works in visuial mode cx.simulate_keystrokes(["shift-v", "down", ">"]); - cx.assert_editor_state("aa\n b«b\n cˇ»c"); + cx.assert_editor_state("aa\n b«b\n ccˇ»"); } #[gpui::test] @@ -157,6 +157,16 @@ async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) { cx.assert_state("aˇbc\n", Mode::Insert); } +#[gpui::test] +async fn test_escape_cancels(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("aˇbˇc", Mode::Normal); + cx.simulate_keystrokes(["escape"]); + + cx.assert_state("aˇbc", Mode::Normal); +} + #[gpui::test] async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 7f9a84b666..023ed880d2 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -61,6 +61,9 @@ pub struct NeovimBackedTestContext<'a> { // bindings are exempted. If None, all bindings are ignored for that insertion text. exemptions: HashMap>>, neovim: NeovimConnection, + + last_set_state: Option, + recent_keystrokes: Vec, } impl<'a> NeovimBackedTestContext<'a> { @@ -71,6 +74,9 @@ impl<'a> NeovimBackedTestContext<'a> { cx, exemptions: Default::default(), neovim: NeovimConnection::new(function_name).await, + + last_set_state: None, + recent_keystrokes: Default::default(), } } @@ -102,13 +108,21 @@ impl<'a> NeovimBackedTestContext<'a> { keystroke_texts: [&str; COUNT], ) -> ContextHandle { for keystroke_text in keystroke_texts.into_iter() { + self.recent_keystrokes.push(keystroke_text.to_string()); self.neovim.send_keystroke(keystroke_text).await; } self.simulate_keystrokes(keystroke_texts) } pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle { - let context_handle = self.set_state(marked_text, Mode::Normal); + let mode = if marked_text.contains("»") { + Mode::Visual { line: false } + } else { + Mode::Normal + }; + let context_handle = self.set_state(marked_text, mode); + self.last_set_state = Some(marked_text.to_string()); + self.recent_keystrokes = Vec::new(); self.neovim.set_state(marked_text).await; context_handle } @@ -116,15 +130,25 @@ impl<'a> NeovimBackedTestContext<'a> { pub async fn assert_shared_state(&mut self, marked_text: &str) { let neovim = self.neovim_state().await; if neovim != marked_text { + let initial_state = self + .last_set_state + .as_ref() + .unwrap_or(&"N/A".to_string()) + .clone(); panic!( indoc! {"Test is incorrect (currently expected != neovim state) - + # initial state: + {} + # keystrokes: + {} # currently expected: {} # neovim state: {} # zed state: {}"}, + initial_state, + self.recent_keystrokes.join(" "), marked_text, neovim, self.editor_state(), @@ -141,28 +165,40 @@ impl<'a> NeovimBackedTestContext<'a> { ) } + pub async fn neovim_mode(&mut self) -> Mode { + self.neovim.mode().await.unwrap() + } + async fn neovim_selection(&mut self) -> Range { - let mut neovim_selection = self.neovim.selection().await; - // Zed selections adjust themselves to make the end point visually make sense - if neovim_selection.start > neovim_selection.end { - neovim_selection.start.column += 1; - } + let neovim_selection = self.neovim.selection().await; neovim_selection.to_offset(&self.buffer_snapshot()) } pub async fn assert_state_matches(&mut self) { - assert_eq!( - self.neovim.text().await, - self.buffer_text(), - "{}", - self.assertion_context() - ); + let neovim = self.neovim_state().await; + let editor = self.editor_state(); + let initial_state = self + .last_set_state + .as_ref() + .unwrap_or(&"N/A".to_string()) + .clone(); - let selections = vec![self.neovim_selection().await]; - self.assert_editor_selections(selections); - - if let Some(neovim_mode) = self.neovim.mode().await { - assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),); + if neovim != editor { + panic!( + indoc! {"Test failed (zed does not match nvim behaviour) + # initial state: + {} + # keystrokes: + {} + # neovim state: + {} + # zed state: + {}"}, + initial_state, + self.recent_keystrokes.join(" "), + neovim, + editor, + ) } } @@ -207,6 +243,29 @@ impl<'a> NeovimBackedTestContext<'a> { } } + pub fn each_marked_position(&self, marked_positions: &str) -> Vec { + let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions); + let mut ret = Vec::with_capacity(cursor_offsets.len()); + + for cursor_offset in cursor_offsets.iter() { + let mut marked_text = unmarked_text.clone(); + marked_text.insert(*cursor_offset, 'ˇ'); + ret.push(marked_text) + } + + ret + } + + pub async fn assert_neovim_compatible( + &mut self, + marked_positions: &str, + keystrokes: [&str; COUNT], + ) { + self.set_shared_state(&marked_positions).await; + self.simulate_shared_keystrokes(keystrokes).await; + self.assert_state_matches().await; + } + pub async fn assert_binding_matches_all_exempted( &mut self, keystrokes: [&str; COUNT], diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 5bfae4e673..dd9be10723 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -213,6 +213,16 @@ impl NeovimConnection { ); } + #[cfg(feature = "neovim")] + async fn read_position(&mut self, cmd: &str) -> u32 { + self.nvim + .command_output(cmd) + .await + .unwrap() + .parse::() + .unwrap() + } + #[cfg(feature = "neovim")] pub async fn state(&mut self) -> (Option, String, Range) { let nvim_buffer = self @@ -226,22 +236,12 @@ impl NeovimConnection { .expect("Could not get buffer text") .join("\n"); - let cursor_row: u32 = self - .nvim - .command_output("echo line('.')") - .await - .unwrap() - .parse::() - .unwrap() - - 1; // Neovim rows start at 1 - let cursor_col: u32 = self - .nvim - .command_output("echo col('.')") - .await - .unwrap() - .parse::() - .unwrap() - - 1; // Neovim columns start at 1 + // nvim columns are 1-based, so -1. + let mut cursor_row = self.read_position("echo line('.')").await - 1; + let mut cursor_col = self.read_position("echo col('.')").await - 1; + let mut selection_row = self.read_position("echo line('v')").await - 1; + let mut selection_col = self.read_position("echo col('v')").await - 1; + let total_rows = self.read_position("echo line('$')").await - 1; let nvim_mode_text = self .nvim @@ -266,46 +266,38 @@ impl NeovimConnection { _ => None, }; - let (start, end) = if let Some(Mode::Visual { .. }) = mode { - self.nvim - .input("") - .await - .expect("Could not exit visual mode"); - let nvim_buffer = self - .nvim - .get_current_buf() - .await - .expect("Could not get neovim buffer"); - let (start_row, start_col) = nvim_buffer - .get_mark("<") - .await - .expect("Could not get selection start"); - let (end_row, end_col) = nvim_buffer - .get_mark(">") - .await - .expect("Could not get selection end"); - self.nvim - .input("gv") - .await - .expect("Could not reselect visual selection"); - - if cursor_row == start_row as u32 - 1 && cursor_col == start_col as u32 { - ( - Point::new(end_row as u32 - 1, end_col as u32), - Point::new(start_row as u32 - 1, start_col as u32), - ) - } else { - ( - Point::new(start_row as u32 - 1, start_col as u32), - Point::new(end_row as u32 - 1, end_col as u32), - ) + // Vim uses the index of the first and last character in the selection + // Zed uses the index of the positions between the characters, so we need + // to add one to the end in visual mode. + match mode { + Some(Mode::Visual { .. }) => { + if selection_col > cursor_col { + let selection_line_length = + self.read_position("echo strlen(getline(line('v')))").await; + if selection_line_length > selection_col { + selection_col += 1; + } else if selection_row < total_rows { + selection_col = 0; + selection_row += 1; + } + } else { + let cursor_line_length = + self.read_position("echo strlen(getline(line('.')))").await; + if cursor_line_length > cursor_col { + cursor_col += 1; + } else if cursor_row < total_rows { + cursor_col = 0; + cursor_row += 1; + } + } } - } else { - ( - Point::new(cursor_row, cursor_col), - Point::new(cursor_row, cursor_col), - ) - }; + Some(Mode::Insert) | Some(Mode::Normal) | None => {} + } + + let (start, end) = ( + Point::new(selection_row, selection_col), + Point::new(cursor_row, cursor_col), + ); let state = NeovimData::Get { mode, diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 5eaaef900e..ab5d7382c7 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -86,12 +86,13 @@ impl<'a> VimTestContext<'a> { pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle { let window = self.window; + let context_handle = self.cx.set_state(text); window.update(self.cx.cx.cx, |cx| { Vim::update(cx, |vim, cx| { - vim.switch_mode(mode, false, cx); + vim.switch_mode(mode, true, cx); }) }); - self.cx.set_state(text) + context_handle } #[track_caller] diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 22bd196c67..e8d69d696c 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -13,7 +13,7 @@ mod visual; use anyhow::Result; use collections::CommandPaletteFilter; -use editor::{Bias, Editor, EditorMode, Event}; +use editor::{movement, Editor, EditorMode, Event}; use gpui::{ actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, @@ -181,6 +181,7 @@ impl Vim { } fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) { + let last_mode = self.state.mode; self.state.mode = mode; self.state.operator_stack.clear(); @@ -197,12 +198,16 @@ impl Vim { self.update_active_editor(cx, |editor, cx| { editor.change_selections(None, cx, |s| { s.move_with(|map, selection| { - if self.state.empty_selections_only() { - let new_head = map.clip_point(selection.head(), Bias::Left); - selection.collapse_to(new_head, selection.goal) - } else { - selection - .set_head(map.clip_point(selection.head(), Bias::Left), selection.goal); + if last_mode.is_visual() && !mode.is_visual() { + let mut point = selection.head(); + if !selection.reversed { + point = movement::left(map, selection.head()); + } + selection.collapse_to(point, selection.goal) + } else if !last_mode.is_visual() && mode.is_visual() { + if selection.is_empty() { + selection.end = movement::right(map, selection.start); + } } }); }) @@ -265,7 +270,7 @@ impl Vim { } Some(Operator::Replace) => match Vim::read(cx).state.mode { Mode::Normal => normal_replace(text, cx), - Mode::Visual { line } => visual_replace(text, line, cx), + Mode::Visual { .. } => visual_replace(text, cx), _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)), }, _ => {} @@ -309,7 +314,7 @@ impl Vim { self.update_active_editor(cx, |editor, cx| { if self.enabled && editor.mode() == EditorMode::Full { editor.set_cursor_shape(cursor_shape, cx); - editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); + editor.set_clip_at_line_ends(state.clip_at_line_ends(), cx); editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled()); editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index d87e4ff974..1716e2d1a5 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -16,10 +16,22 @@ use crate::{ Vim, }; -actions!(vim, [VisualDelete, VisualChange, VisualYank, VisualPaste]); +actions!( + vim, + [ + ToggleVisual, + ToggleVisualLine, + VisualDelete, + VisualYank, + VisualPaste, + OtherEnd, + ] +); pub fn init(cx: &mut AppContext) { - cx.add_action(change); + cx.add_action(toggle_visual); + cx.add_action(toggle_visual_line); + cx.add_action(other_end); cx.add_action(delete); cx.add_action(yank); cx.add_action(paste); @@ -32,24 +44,45 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex s.move_with(|map, selection| { let was_reversed = selection.reversed; - if let Some((new_head, goal)) = - motion.move_point(map, selection.head(), selection.goal, times) - { - selection.set_head(new_head, goal); + let mut current_head = selection.head(); - if was_reversed && !selection.reversed { - // Head was at the start of the selection, and now is at the end. We need to move the start - // back by one if possible in order to compensate for this change. - *selection.start.column_mut() = - selection.start.column().saturating_sub(1); - selection.start = map.clip_point(selection.start, Bias::Left); - } else if !was_reversed && selection.reversed { - // Head was at the end of the selection, and now is at the start. We need to move the end - // forward by one if possible in order to compensate for this change. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Right); + // our motions assume the current character is after the cursor, + // but in (forward) visual mode the current character is just + // before the end of the selection. + + // If the file ends with a newline (which is common) we don't do this. + // so that if you go to the end of such a file you can use "up" to go + // to the previous line and have it work somewhat as expected. + if !selection.reversed + && !selection.is_empty() + && !(selection.end.column() == 0 && selection.end == map.max_point()) + { + current_head = movement::left(map, selection.end) + } + + let Some((new_head, goal)) = + motion.move_point(map, current_head, selection.goal, times) else { return }; + + selection.set_head(new_head, goal); + + // ensure the current character is included in the selection. + if !selection.reversed { + // TODO: maybe try clipping left for multi-buffers + let next_point = movement::right(map, selection.end); + + if !(next_point.column() == 0 && next_point == map.max_point()) { + selection.end = movement::right(map, selection.end) } } + + // vim always ensures the anchor character stays selected. + // if our selection has reversed, we need to move the opposite end + // to ensure the anchor is still selected. + if was_reversed && !selection.reversed { + selection.start = movement::left(map, selection.start); + } else if !was_reversed && selection.reversed { + selection.end = movement::right(map, selection.end); + } }); }); }); @@ -64,14 +97,29 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { - let head = selection.head(); - if let Some(mut range) = object.range(map, head, around) { - if !range.is_empty() { - if let Some((_, end)) = map.reverse_chars_at(range.end).next() { - range.end = end; - } + let mut head = selection.head(); - if selection.is_empty() { + // all our motions assume that the current character is + // after the cursor; however in the case of a visual selection + // the current character is before the cursor. + if !selection.reversed { + head = movement::left(map, head); + } + + if let Some(range) = object.range(map, head, around) { + if !range.is_empty() { + let expand_both_ways = if selection.is_empty() { + true + // contains only one character + } else if let Some((_, start)) = + map.reverse_chars_at(selection.end).next() + { + selection.start == start + } else { + false + }; + + if expand_both_ways { selection.start = range.start; selection.end = range.end; } else if selection.reversed { @@ -88,72 +136,58 @@ pub fn visual_object(object: Object, cx: &mut WindowContext) { }); } -pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext) { +pub fn toggle_visual(_: &mut Workspace, _: &ToggleVisual, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| match vim.state.mode { + Mode::Normal | Mode::Insert | Mode::Visual { line: true } => { + vim.switch_mode(Mode::Visual { line: false }, false, cx); + } + Mode::Visual { line: false } => { + vim.switch_mode(Mode::Normal, false, cx); + } + }) +} + +pub fn toggle_visual_line( + _: &mut Workspace, + _: &ToggleVisualLine, + cx: &mut ViewContext, +) { + Vim::update(cx, |vim, cx| match vim.state.mode { + Mode::Normal | Mode::Insert | Mode::Visual { line: false } => { + vim.switch_mode(Mode::Visual { line: true }, false, cx); + } + Mode::Visual { line: true } => { + vim.switch_mode(Mode::Normal, false, cx); + } + }) +} + +pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - // Compute edits and resulting anchor selections. If in line mode, adjust - // the anchor location and additional newline - let mut edits = Vec::new(); - let mut new_selections = Vec::new(); - let line_mode = editor.selections.line_mode; editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Right); - } - - if line_mode { - let range = selection.map(|p| p.to_point(map)).range(); - let expanded_range = map.expand_to_line(range); - // If we are at the last line, the anchor needs to be after the newline so that - // it is on a line of its own. Otherwise, the anchor may be after the newline - let anchor = if expanded_range.end == map.buffer_snapshot.max_point() { - map.buffer_snapshot.anchor_after(expanded_range.end) - } else { - map.buffer_snapshot.anchor_before(expanded_range.start) - }; - - edits.push((expanded_range, "\n")); - new_selections.push(selection.map(|_| anchor)); - } else { - let range = selection.map(|p| p.to_point(map)).range(); - let anchor = map.buffer_snapshot.anchor_after(range.end); - edits.push((range, "")); - new_selections.push(selection.map(|_| anchor)); - } - selection.goal = SelectionGoal::None; - }); - }); - copy_selections_content(editor, editor.selections.line_mode, cx); - editor.edit_with_autoindent(edits, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_anchors(new_selections); - }); - }); - vim.switch_mode(Mode::Insert, false, cx); + s.move_with(|_, selection| { + selection.reversed = !selection.reversed; + }) + }) + }) }); } pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); let mut original_columns: HashMap<_, _> = Default::default(); let line_mode = editor.selections.line_mode; + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { if line_mode { - original_columns - .insert(selection.id, selection.head().to_point(map).column); - } else if !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Right); + let mut position = selection.head(); + if !selection.reversed { + position = movement::left(map, position); + } + original_columns.insert(selection.id, position.to_point(map).column); } selection.goal = SelectionGoal::None; }); @@ -175,27 +209,14 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); let line_mode = editor.selections.line_mode; - if !line_mode { - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = map.clip_point(selection.end, Bias::Right); - } - }); - }); - } copy_selections_content(editor, line_mode, cx); editor.change_selections(None, cx, |s| { s.move_with(|_, selection| { @@ -203,7 +224,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext) }); }); }); - vim.switch_mode(Mode::Normal, false, cx); + vim.switch_mode(Mode::Normal, true, cx); }); } @@ -256,11 +277,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext let mut selection = selection.clone(); if !selection.reversed { - let mut adjusted = selection.end; - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *adjusted.column_mut() = adjusted.column() + 1; - adjusted = display_map.clip_point(adjusted, Bias::Right); + let adjusted = selection.end; // If the selection is empty, move both the start and end forward one // character if selection.is_empty() { @@ -311,11 +328,11 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext } }); }); - vim.switch_mode(Mode::Normal, false, cx); + vim.switch_mode(Mode::Normal, true, cx); }); } -pub(crate) fn visual_replace(text: Arc, line: bool, cx: &mut WindowContext) { +pub(crate) fn visual_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { @@ -336,14 +353,7 @@ pub(crate) fn visual_replace(text: Arc, line: bool, cx: &mut WindowContext) let mut edits = Vec::new(); for selection in selections.iter() { - let mut selection = selection.clone(); - if !line && !selection.reversed { - // Head is at the end of the selection. Adjust the end position to - // to include the character under the cursor. - *selection.end.column_mut() = selection.end.column() + 1; - selection.end = display_map.clip_point(selection.end, Bias::Right); - } - + let selection = selection.clone(); for row_range in movement::split_display_range_by_lines(&display_map, selection.range()) { @@ -367,6 +377,7 @@ pub(crate) fn visual_replace(text: Arc, line: bool, cx: &mut WindowContext) #[cfg(test)] mod test { use indoc::indoc; + use workspace::item::Item; use crate::{ state::Mode, @@ -375,19 +386,146 @@ mod test { #[gpui::test] async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["v", "w", "j"]); - cx.assert_all(indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}) + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "The ˇquick brown + fox jumps over + the lazy dog" + }) + .await; + let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + + // entering visual mode should select the character + // under cursor + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! { "The «qˇ»uick brown + fox jumps over + the lazy dog"}) .await; - let mut cx = cx.binding(["v", "b", "k"]); - cx.assert_all(indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}) + cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + + // forwards motions should extend the selection + cx.simulate_shared_keystrokes(["w", "j"]).await; + cx.assert_shared_state(indoc! { "The «quick brown + fox jumps oˇ»ver + the lazy dog"}) + .await; + + cx.simulate_shared_keystrokes(["escape"]).await; + assert_eq!(Mode::Normal, cx.neovim_mode().await); + cx.assert_shared_state(indoc! { "The quick brown + fox jumps ˇover + the lazy dog"}) + .await; + + // motions work backwards + cx.simulate_shared_keystrokes(["v", "k", "b"]).await; + cx.assert_shared_state(indoc! { "The «ˇquick brown + fox jumps o»ver + the lazy dog"}) + .await; + + // works on empty lines + cx.set_shared_state(indoc! {" + a + ˇ + b + "}) + .await; + let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! {" + a + « + ˇ»b + "}) + .await; + cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + + // toggles off again + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! {" + a + ˇ + b + "}) + .await; + + // works at the end of a document + cx.set_shared_state(indoc! {" + a + b + ˇ"}) + .await; + + cx.simulate_shared_keystrokes(["v"]).await; + cx.assert_shared_state(indoc! {" + a + b + ˇ"}) + .await; + assert_eq!(cx.mode(), cx.neovim_mode().await); + } + + #[gpui::test] + async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! { + "The ˇquick brown + fox jumps over + the lazy dog" + }) + .await; + cx.simulate_shared_keystrokes(["shift-v"]).await; + cx.assert_shared_state(indoc! { "The «qˇ»uick brown + fox jumps over + the lazy dog"}) + .await; + assert_eq!(cx.mode(), cx.neovim_mode().await); + cx.simulate_shared_keystrokes(["x"]).await; + cx.assert_shared_state(indoc! { "fox ˇjumps over + the lazy dog"}) + .await; + + // it should work on empty lines + cx.set_shared_state(indoc! {" + a + ˇ + b"}) + .await; + cx.simulate_shared_keystrokes(["shift-v"]).await; + cx.assert_shared_state(indoc! { " + a + « + ˇ»b"}) + .await; + cx.simulate_shared_keystrokes(["x"]).await; + cx.assert_shared_state(indoc! { " + a + ˇb"}) + .await; + + // it should work at the end of the document + cx.set_shared_state(indoc! {" + a + b + ˇ"}) + .await; + let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + cx.simulate_shared_keystrokes(["shift-v"]).await; + cx.assert_shared_state(indoc! {" + a + b + ˇ"}) + .await; + assert_eq!(cx.mode(), cx.neovim_mode().await); + cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.simulate_shared_keystrokes(["x"]).await; + cx.assert_shared_state(indoc! {" + a + ˇb"}) .await; } @@ -395,6 +533,9 @@ mod test { async fn test_visual_delete(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; + cx.assert_binding_matches(["v", "w"], "The quick ˇbrown") + .await; + cx.assert_binding_matches(["v", "w", "x"], "The quick ˇbrown") .await; cx.assert_binding_matches( @@ -457,62 +598,15 @@ mod test { fox juˇmps over the laˇzy dog"}) .await; - } - #[gpui::test] - async fn test_visual_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["v", "w", "c"]); - cx.assert("The quick ˇbrown").await; - let mut cx = cx.binding(["v", "w", "j", "c"]); - cx.assert_all(indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}) + cx.set_shared_state(indoc! {" + The ˇlong line + should not + crash + "}) .await; - let mut cx = cx.binding(["v", "b", "k", "c"]); - cx.assert_all(indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}) - .await; - } - - #[gpui::test] - async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["shift-v", "c"]); - cx.assert(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; - // Test pasting code copied on change - cx.simulate_shared_keystrokes(["escape", "j", "p"]).await; + cx.simulate_shared_keystrokes(["shift-v", "$", "x"]).await; cx.assert_state_matches().await; - - cx.assert_all(indoc! {" - The quick brown - fox juˇmps over - the laˇzy dog"}) - .await; - let mut cx = cx.binding(["shift-v", "j", "c"]); - cx.assert(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; - // Test pasting code copied on delete - cx.simulate_shared_keystrokes(["escape", "j", "p"]).await; - cx.assert_state_matches().await; - - cx.assert_all(indoc! {" - The quick brown - fox juˇmps over - the laˇzy dog"}) - .await; } #[gpui::test] @@ -605,7 +699,7 @@ mod test { cx.set_state( indoc! {" The quick brown - fox «jumpˇ»s over + fox «jumpsˇ» over the lazy dog"}, Mode::Visual { line: false }, ); @@ -629,7 +723,7 @@ mod test { cx.set_state( indoc! {" The quick brown - fox juˇmps over + fox ju«mˇ»ps over the lazy dog"}, Mode::Visual { line: true }, ); @@ -643,7 +737,7 @@ mod test { cx.set_state( indoc! {" The quick brown - the «lazˇ»y dog"}, + the «lazyˇ» dog"}, Mode::Visual { line: false }, ); cx.simulate_keystroke("p"); diff --git a/crates/vim/test_data/test_enter_visual_line_mode.json b/crates/vim/test_data/test_enter_visual_line_mode.json new file mode 100644 index 0000000000..6769145412 --- /dev/null +++ b/crates/vim/test_data/test_enter_visual_line_mode.json @@ -0,0 +1,15 @@ +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"shift-v"} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":true}}}} +{"Key":"x"} +{"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}} +{"Put":{"state":"a\nˇ\nb"}} +{"Key":"shift-v"} +{"Get":{"state":"a\n«\nˇ»b","mode":{"Visual":{"line":true}}}} +{"Key":"x"} +{"Get":{"state":"a\nˇb","mode":"Normal"}} +{"Put":{"state":"a\nb\nˇ"}} +{"Key":"shift-v"} +{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":true}}}} +{"Key":"x"} +{"Get":{"state":"a\nˇb","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter_visual_mode.json b/crates/vim/test_data/test_enter_visual_mode.json index bd4e91977f..4fdb4c7667 100644 --- a/crates/vim/test_data/test_enter_visual_mode.json +++ b/crates/vim/test_data/test_enter_visual_mode.json @@ -1,30 +1,20 @@ {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} {"Key":"v"} +{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}} {"Key":"w"} {"Key":"j"} -{"Get":{"state":"The «quick brown\nfox jumps ˇ»over\nthe lazy dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} +{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Key":"escape"} +{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}} {"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox jumps «over\nˇ»the lazy dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe «lazy ˇ»dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} {"Key":"k"} -{"Get":{"state":"«ˇThe »quick brown\nfox jumps over\nthe lazy dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} {"Key":"b"} -{"Key":"k"} -{"Get":{"state":"The «ˇquick brown\nfox jumps »over\nthe lazy dog","mode":{"Visual":{"line":false}}}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} +{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":{"Visual":{"line":false}}}} +{"Put":{"state":"a\nˇ\nb\n"}} {"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Get":{"state":"The quick brown\n«ˇfox jumps over\nthe »lazy dog","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"a\n«\nˇ»b\n","mode":{"Visual":{"line":false}}}} +{"Key":"v"} +{"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}} +{"Put":{"state":"a\nb\nˇ"}} +{"Key":"v"} +{"Get":{"state":"a\nb\nˇ","mode":{"Visual":{"line":false}}}} diff --git a/crates/vim/test_data/test_multiline_surrounding_character_objects.json b/crates/vim/test_data/test_multiline_surrounding_character_objects.json new file mode 100644 index 0000000000..f683c0a314 --- /dev/null +++ b/crates/vim/test_data/test_multiline_surrounding_character_objects.json @@ -0,0 +1,10 @@ +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n return true\n }\n ˇreturn false\n}"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"{"} +{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":{"Visual":{"line":false}}}} +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"{"} +{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":{"Visual":{"line":false}}}} diff --git a/crates/vim/test_data/test_visual_change.json b/crates/vim/test_data/test_visual_change.json index 8c252e49c5..7d1efe05ce 100644 --- a/crates/vim/test_data/test_visual_change.json +++ b/crates/vim/test_data/test_visual_change.json @@ -9,33 +9,39 @@ {"Key":"j"} {"Key":"c"} {"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"j"} +{"Key":"c"} +{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"k"} +{"Key":"c"} +{"Get":{"state":"The ˇrown\nfox jumps over\nthe lazy dog","mode":"Insert"}} {"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} {"Key":"v"} {"Key":"w"} {"Key":"j"} {"Key":"c"} {"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}} +{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"k"} +{"Key":"c"} +{"Get":{"state":"The quick brown\nˇver\nthe lazy dog","mode":"Insert"}} {"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} {"Key":"v"} {"Key":"w"} {"Key":"j"} {"Key":"c"} {"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} {"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} {"Key":"v"} -{"Key":"b"} +{"Key":"w"} {"Key":"k"} {"Key":"c"} -{"Get":{"state":"The quick brown\nˇazy dog","mode":"Insert"}} +{"Get":{"state":"The quick brown\nfox jumpsˇazy dog","mode":"Insert"}} diff --git a/crates/vim/test_data/test_visual_delete.json b/crates/vim/test_data/test_visual_delete.json index 42d7a69849..df025f48a0 100644 --- a/crates/vim/test_data/test_visual_delete.json +++ b/crates/vim/test_data/test_visual_delete.json @@ -1,6 +1,10 @@ {"Put":{"state":"The quick ˇbrown"}} {"Key":"v"} {"Key":"w"} +{"Get":{"state":"The quick «brownˇ»","mode":{"Visual":{"line":false}}}} +{"Put":{"state":"The quick ˇbrown"}} +{"Key":"v"} +{"Key":"w"} {"Key":"x"} {"Get":{"state":"The quickˇ ","mode":"Normal"}} {"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} diff --git a/crates/vim/test_data/test_visual_line_delete.json b/crates/vim/test_data/test_visual_line_delete.json index 4b8248235e..51406266f6 100644 --- a/crates/vim/test_data/test_visual_line_delete.json +++ b/crates/vim/test_data/test_visual_line_delete.json @@ -29,3 +29,8 @@ {"Key":"j"} {"Key":"x"} {"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}} +{"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}} +{"Key":"shift-v"} +{"Key":"$"} +{"Key":"x"} +{"Get":{"state":"should noˇt\ncrash\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json index 5514f7385a..b1c43bf9a2 100644 --- a/crates/vim/test_data/test_visual_word_object.json +++ b/crates/vim/test_data/test_visual_word_object.json @@ -1,230 +1,236 @@ +{"Put":{"state":"The quick ˇbrown\nfox"}} +{"Key":"v"} +{"Get":{"state":"The quick «bˇ»rown\nfox","mode":{"Visual":{"line":false}}}} +{"Key":"i"} +{"Key":"w"} +{"Get":{"state":"The quick «brownˇ»\nfox","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «browˇ»n \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick «browˇ»n \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown« ˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpˇ»s over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpˇ»s over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ» \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Thˇ»e-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe«-ˇ»quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «browˇ»n \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ» \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ» \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ» fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpˇ»s over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} {"Key":"v"} {"Key":"i"} {"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick «browˇ»n \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick «browˇ»n \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown« ˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpˇ»s over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpˇ»s over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ» \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quicˇ»k brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «browˇ»n \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ» \n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ» \n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ» fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpˇ»s over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":{"Visual":{"line":false}}}} {"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} {"Key":"v"} {"Key":"i"} {"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":{"Visual":{"line":false}}}} +{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":{"Visual":{"line":false}}}} diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index e33c0a5391..641eae081e 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -497,9 +497,8 @@ impl View for PanelButtons { }; Stack::new() .with_child( - MouseEventHandler::::new(panel_ix, cx, |state, cx| { + MouseEventHandler::new::(panel_ix, cx, |state, cx| { let style = button_style.in_state(is_active); - let style = style.style_for(state); Flex::row() .with_child( diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 09cfb4d5d8..55b44e9673 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -290,7 +290,7 @@ pub mod simple_message_notification { .flex(1., true), ) .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) @@ -319,7 +319,7 @@ pub mod simple_message_notification { .with_children({ click_message .map(|click_message| { - MouseEventHandler::::new( + MouseEventHandler::new::( 0, cx, |state, _| { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 51e2da3f6a..dee6701564 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -234,7 +234,7 @@ fn nav_button)>( action_name: &str, cx: &mut ViewContext, ) -> AnyElement { - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::new::(0, cx, |state, _| { let style = if enabled { style.style_for(state) } else { @@ -1317,7 +1317,7 @@ impl Pane { enum Tab {} let mouse_event_handler = - MouseEventHandler::::new(ix, cx, |_, cx| { + MouseEventHandler::new::(ix, cx, |_, cx| { Self::render_tab( &item, pane.clone(), @@ -1526,7 +1526,7 @@ impl Pane { let item_id = item.id(); enum TabCloseButton {} let icon = Svg::new("icons/x_mark_8.svg"); - MouseEventHandler::::new(item_id, cx, |mouse_state, _| { + MouseEventHandler::new::(item_id, cx, |mouse_state, _| { if mouse_state.hovered() { icon.with_color(tab_style.icon_close_active) } else { @@ -1591,7 +1591,7 @@ impl Pane { ) -> AnyElement { enum TabBarButton {} - let mut button = MouseEventHandler::::new(index, cx, |mouse_state, cx| { + let mut button = MouseEventHandler::new::(index, cx, |mouse_state, cx| { let theme = &settings::get::(cx).theme.workspace.tab_bar; let style = theme.pane_button.in_state(is_active).style_for(mouse_state); Svg::new(icon) @@ -1653,7 +1653,7 @@ impl View for Pane { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { enum MouseNavigationHandler {} - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let active_item_index = self.active_item_index; if let Some(active_item) = self.active_item() { @@ -1665,7 +1665,7 @@ impl View for Pane { enum TabBarEventHandler {} stack.add_child( - MouseEventHandler::::new(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Empty::new() .contained() .with_style(theme.workspace.tab_bar.container) diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 165537a1af..1cbf3e4f50 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -19,7 +19,7 @@ pub fn dragged_item_receiver( split_margin: Option, cx: &mut ViewContext, render_child: F, -) -> MouseEventHandler +) -> MouseEventHandler where Tag: 'static, D: Element, @@ -39,7 +39,7 @@ where None }; - let mut handler = MouseEventHandler::::above(region_id, cx, |state, cx| { + let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { // Observing hovered will cause a render when the mouse enters regardless // of if mouse position was accessed before let drag_position = if state.hovered() { drag_position } else { None }; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index dfda5092ca..7528fb7468 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -212,7 +212,7 @@ impl Member { let leader_user_id = leader.user.id; let app_state = Arc::downgrade(app_state); Some( - MouseEventHandler::::new( + MouseEventHandler::new::( pane.id(), cx, |_, _| { diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index 977e167f60..aea03df5e0 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -72,7 +72,7 @@ impl View for SharedScreen { enum Focus {} let frame = self.frame.clone(); - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Canvas::new(move |scene, bounds, _, _, _| { if let Some(frame) = frame.clone() { let size = constrain_size_preserving_aspect_ratio( diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index e8c1240d43..72c879d6d4 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -143,6 +143,61 @@ impl View for Toolbar { } } +// <<<<<<< HEAD +// ======= +// #[allow(clippy::too_many_arguments)] +// fn nav_button)>( +// svg_path: &'static str, +// style: theme::Interactive, +// nav_button_height: f32, +// tooltip_style: TooltipStyle, +// enabled: bool, +// spacing: f32, +// on_click: F, +// tooltip_action: A, +// action_name: &'static str, +// cx: &mut ViewContext, +// ) -> AnyElement { +// MouseEventHandler::new::(0, cx, |state, _| { +// let style = if enabled { +// style.style_for(state) +// } else { +// style.disabled_style() +// }; +// Svg::new(svg_path) +// .with_color(style.color) +// .constrained() +// .with_width(style.icon_width) +// .aligned() +// .contained() +// .with_style(style.container) +// .constrained() +// .with_width(style.button_width) +// .with_height(nav_button_height) +// .aligned() +// .top() +// }) +// .with_cursor_style(if enabled { +// CursorStyle::PointingHand +// } else { +// CursorStyle::default() +// }) +// .on_click(MouseButton::Left, move |_, toolbar, cx| { +// on_click(toolbar, cx) +// }) +// .with_tooltip::( +// 0, +// action_name, +// Some(Box::new(tooltip_action)), +// tooltip_style, +// cx, +// ) +// .contained() +// .with_margin_right(spacing) +// .into_any_named("nav button") +// } + +// >>>>>>> 139cbbfd3aebd0863a7d51b0c12d748764cf0b2e impl Toolbar { pub fn new() -> Self { Self { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ab4f7286dc..a449c58de3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2560,7 +2560,7 @@ impl Workspace { }; enum TitleBar {} - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { Stack::new() .with_children( self.titlebar_item @@ -2649,7 +2649,7 @@ impl Workspace { if self.project.read(cx).is_read_only() { enum DisconnectedOverlay {} Some( - MouseEventHandler::::new(0, cx, |_, cx| { + MouseEventHandler::new::(0, cx, |_, cx| { let theme = &theme::current(cx); Label::new( "Your connection to the remote project has been lost.", diff --git a/crates/zed/src/languages/cpp/config.toml b/crates/zed/src/languages/cpp/config.toml index c719fae1eb..d9b38bca06 100644 --- a/crates/zed/src/languages/cpp/config.toml +++ b/crates/zed/src/languages/cpp/config.toml @@ -1,5 +1,5 @@ name = "C++" -path_suffixes = ["cc", "cpp", "h", "hpp"] +path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"] line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index c23ddcd6e7..8f4670388e 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -1,5 +1,5 @@ name = "JavaScript" -path_suffixes = ["js", "jsx", "mjs"] +path_suffixes = ["js", "jsx", "mjs", "cjs"] first_line_pattern = '^#!.*\bnode\b' line_comment = "// " autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/python/config.toml b/crates/zed/src/languages/python/config.toml index 80609de0ba..6777f6e60d 100644 --- a/crates/zed/src/languages/python/config.toml +++ b/crates/zed/src/languages/python/config.toml @@ -1,5 +1,5 @@ name = "Python" -path_suffixes = ["py", "pyi"] +path_suffixes = ["py", "pyi", "mpy"] first_line_pattern = '^#!.*\bpython[0-9.]*\b' line_comment = "# " autoclose_before = ";:.,=}])>" diff --git a/crates/zed/src/languages/typescript/config.toml b/crates/zed/src/languages/typescript/config.toml index 4f006b342a..a2b764d9fe 100644 --- a/crates/zed/src/languages/typescript/config.toml +++ b/crates/zed/src/languages/typescript/config.toml @@ -1,5 +1,5 @@ name = "TypeScript" -path_suffixes = ["ts"] +path_suffixes = ["ts", "cts", "d.cts", "d.mts", "mts"] line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [