From 3178adefdeab488fb3068c198d8670030e6cf433 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 14:54:05 -0700 Subject: [PATCH 001/142] WIP: Add disclosable channels --- crates/collab_ui/src/collab_panel.rs | 91 ++++++++++++++++++++++++++- crates/gpui/src/elements.rs | 7 +++ crates/gpui/src/elements/component.rs | 7 +++ crates/search/src/search.rs | 21 +++---- crates/theme/src/components.rs | 46 +++++++------- 5 files changed, 136 insertions(+), 36 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c49011b86b7b17d2d73c7171676962ddef9e9cb7..16de60e735a04d5c3cecb742e7cead60320b4ae6 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -8,6 +8,7 @@ use client::{ proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, }; +use components::DisclosureExt; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -16,7 +17,7 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, + Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, }, geometry::{ @@ -1615,6 +1616,10 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) + .component() + .styleable() + .disclosable() + .into_element() .into_any() } @@ -2522,3 +2527,87 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .contained() .with_style(style.container) } + +mod components { + + use gpui::{ + elements::{Empty, Flex, GeneralComponent, ParentElement, StyleableComponent}, + Action, Element, + }; + use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, + }; + + #[derive(Clone)] + struct DisclosureStyle { + disclosure: ToggleIconButtonStyle, + spacing: f32, + content: S, + } + + struct Disclosable { + disclosed: bool, + action: Box, + content: C, + style: S, + } + + impl Disclosable<(), ()> { + fn new(disclosed: bool, content: C, action: Box) -> Disclosable { + Disclosable { + disclosed, + content, + action, + style: (), + } + } + } + + impl StyleableComponent for Disclosable { + type Style = DisclosureStyle; + + type Output = Disclosable; + + fn with_style(self, style: Self::Style) -> Self::Output { + Disclosable { + disclosed: self.disclosed, + action: self.action, + content: self.content, + style, + } + } + } + + impl GeneralComponent for Disclosable> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child( + ActionButton::new_dynamic(self.action) + .with_contents(Svg::new("path")) + .toggleable(self.disclosed) + .with_style(self.style.disclosure) + .element(), + ) + .with_child(Empty::new().constrained().with_width(self.style.spacing)) + .with_child(self.content.with_style(self.style.content).render(v, cx)) + .align_children_center() + .into_any() + } + } + + pub trait DisclosureExt { + fn disclosable(self, disclosed: bool, action: Box) -> Disclosable + where + Self: Sized; + } + + impl DisclosureExt for C { + fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { + Disclosable::new(disclosed, self, action) + } + } +} diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 03caae8dd9b6591741de99a5610049caf6991760..f7697d6fc13b08ac09a84ed5e2051c9cec286b6d 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -229,6 +229,13 @@ pub trait Element: 'static { { MouseEventHandler::for_child::(self.into_any(), region_id) } + + fn component(self) -> ElementAdapter + where + Self: Sized, + { + ElementAdapter::new(self.into_any()) + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index e2770c014859c902cc86fe1f04590f81595c94c5..ee4702a6fad308feaa148884af848a4e3148ce2e 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -50,6 +50,13 @@ pub trait Component { { ComponentAdapter::new(self) } + + fn styleable(self) -> StylableComponentAdapter + where + Self: Sized, + { + StylableComponentAdapter::new(self) + } } impl Component for C { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 8d8c02c8d7e0fbff84a509ba8a9fbf6233965adb..2dc45e397394b84671533471e7c93937d9469354 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -8,7 +8,9 @@ use gpui::{ pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, +}; pub mod buffer_search; mod history; @@ -89,15 +91,12 @@ impl SearchOptions { tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { - ActionButton::new_dynamic( - self.to_toggle_action(), - format!("Toggle {}", self.label()), - tooltip_style, - ) - .with_contents(theme::components::svg::Svg::new(self.icon())) - .toggleable(active) - .with_style(button_style) - .element() - .into_any() + ActionButton::new_dynamic(self.to_toggle_action()) + .with_tooltip(format!("Toggle {}", self.label()), tooltip_style) + .with_contents(Svg::new(self.icon())) + .toggleable(active) + .with_style(button_style) + .element() + .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fce7ad825cc2ede0a8501dba5ac4fe7ff520bc08..1e395405cbb506596248d51ac5b387ca2a654e83 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -81,8 +81,7 @@ pub mod action_button { pub struct ActionButton { action: Box, - tooltip: Cow<'static, str>, - tooltip_style: TooltipStyle, + tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, contents: C, style: Interactive, @@ -99,27 +98,27 @@ pub mod action_button { } impl ActionButton<(), ()> { - pub fn new_dynamic( - action: Box, - tooltip: impl Into>, - tooltip_style: TooltipStyle, - ) -> Self { + pub fn new_dynamic(action: Box) -> Self { Self { contents: (), tag: action.type_tag(), style: Interactive::new_blank(), - tooltip: tooltip.into(), - tooltip_style, + tooltip: None, action, } } - pub fn new( - action: A, + pub fn new(action: A) -> Self { + Self::new_dynamic(Box::new(action)) + } + + pub fn with_tooltip( + mut self, tooltip: impl Into>, tooltip_style: TooltipStyle, ) -> Self { - Self::new_dynamic(Box::new(action), tooltip, tooltip_style) + self.tooltip = Some((tooltip.into(), tooltip_style)); + self } pub fn with_contents(self, contents: C) -> ActionButton { @@ -128,7 +127,6 @@ pub mod action_button { tag: self.tag, style: self.style, tooltip: self.tooltip, - tooltip_style: self.tooltip_style, contents, } } @@ -144,7 +142,7 @@ pub mod action_button { tag: self.tag, contents: self.contents, tooltip: self.tooltip, - tooltip_style: self.tooltip_style, + style, } } @@ -152,7 +150,7 @@ pub mod action_button { impl GeneralComponent for ActionButton> { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { + let mut button = MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents @@ -180,15 +178,15 @@ pub mod action_button { } }) .with_cursor_style(CursorStyle::PointingHand) - .with_dynamic_tooltip( - self.tag, - 0, - self.tooltip, - Some(self.action), - self.tooltip_style, - cx, - ) - .into_any() + .into_any(); + + if let Some((tooltip, style)) = self.tooltip { + button = button + .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx) + .into_any() + } + + button } } } From 2d37128693b40b3793d4ca6238e93b5e24c2ec88 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 19:02:27 -0700 Subject: [PATCH 002/142] Actually get it compiling, omg --- crates/collab_ui/src/collab_panel.rs | 38 +++++++----- crates/gpui/examples/components.rs | 4 +- crates/gpui/src/elements.rs | 7 +++ crates/gpui/src/elements/component.rs | 85 ++++++++++++++++++++++++--- crates/search/src/search.rs | 4 +- crates/theme/src/components.rs | 34 ++++++----- 6 files changed, 131 insertions(+), 41 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16de60e735a04d5c3cecb742e7cead60320b4ae6..f35c07d5aa0a9e98a8a24fee93c895308f7a09bb 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -8,7 +8,6 @@ use client::{ proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, }; -use components::DisclosureExt; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -17,8 +16,9 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, + Canvas, ChildView, Empty, Flex, GeneralComponent, GeneralStyleableComponent, Image, Label, + List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, + ParentElement, Stack, Svg, }, geometry::{ rect::RectF, @@ -44,7 +44,10 @@ use workspace::{ Workspace, }; -use crate::face_pile::FacePile; +use crate::{ + collab_panel::components::{DisclosureExt, DisclosureStyle}, + face_pile::FacePile, +}; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -1616,10 +1619,17 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) - .component() - .styleable() - .disclosable() - .into_element() + .dynamic_component() + .stylable() + .disclosable(true, Box::new(RemoveChannel { channel_id: 0 })) + .with_style({ + fn style() -> DisclosureStyle<()> { + todo!() + } + + style() + }) + .element() .into_any() } @@ -2531,7 +2541,7 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen mod components { use gpui::{ - elements::{Empty, Flex, GeneralComponent, ParentElement, StyleableComponent}, + elements::{Empty, Flex, GeneralComponent, GeneralStyleableComponent, ParentElement}, Action, Element, }; use theme::components::{ @@ -2539,13 +2549,13 @@ mod components { }; #[derive(Clone)] - struct DisclosureStyle { + pub struct DisclosureStyle { disclosure: ToggleIconButtonStyle, spacing: f32, content: S, } - struct Disclosable { + pub struct Disclosable { disclosed: bool, action: Box, content: C, @@ -2563,7 +2573,7 @@ mod components { } } - impl StyleableComponent for Disclosable { + impl GeneralStyleableComponent for Disclosable { type Style = DisclosureStyle; type Output = Disclosable; @@ -2578,7 +2588,7 @@ mod components { } } - impl GeneralComponent for Disclosable> { + impl GeneralComponent for Disclosable> { fn render( self, v: &mut V, @@ -2605,7 +2615,7 @@ mod components { Self: Sized; } - impl DisclosureExt for C { + impl DisclosureExt for C { fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { Disclosable::new(disclosed, self, action) } diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index ad38b5893c48a831c245ed61300d9c92d7319383..aeeab8101d1e926f287e72c9fd22a44af8d645df 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .element(), + .c_element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .element(), + .c_element(), ) .expanded() .contained() diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index f7697d6fc13b08ac09a84ed5e2051c9cec286b6d..c0484ef9395a9e25e82384b30aa5fdc88d81bf07 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -236,6 +236,13 @@ pub trait Element: 'static { { ElementAdapter::new(self.into_any()) } + + fn dynamic_component(self) -> DynamicElementAdapter + where + Self: Sized, + { + DynamicElementAdapter::new(self.into_any()) + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index ee4702a6fad308feaa148884af848a4e3148ce2e..f5a4180d7f379c059a175c731311ca46360f0132 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{any::Any, marker::PhantomData}; use pathfinder_geometry::{rect::RectF, vector::Vector2F}; @@ -11,15 +11,43 @@ use super::Empty; pub trait GeneralComponent { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + fn element(self) -> ComponentAdapter where Self: Sized, { ComponentAdapter::new(self) } + + fn stylable(self) -> GeneralStylableComponentAdapter + where + Self: Sized, + { + GeneralStylableComponentAdapter::new(self) + } +} + +pub struct GeneralStylableComponentAdapter { + component: C, +} + +impl GeneralStylableComponentAdapter { + pub fn new(component: C) -> Self { + Self { component } + } +} + +impl GeneralStyleableComponent for GeneralStylableComponentAdapter { + type Style = (); + + type Output = C; + + fn with_style(self, _: Self::Style) -> Self::Output { + self.component + } } -pub trait StyleableComponent { +pub trait GeneralStyleableComponent { type Style: Clone; type Output: GeneralComponent; @@ -32,7 +60,7 @@ impl GeneralComponent for () { } } -impl StyleableComponent for () { +impl GeneralStyleableComponent for () { type Style = (); type Output = (); @@ -41,17 +69,34 @@ impl StyleableComponent for () { } } +pub trait StyleableComponent { + type Style: Clone; + type Output: Component; + + fn c_with_style(self, style: Self::Style) -> Self::Output; +} + +impl StyleableComponent for C { + type Style = C::Style; + + type Output = C::Output; + + fn c_with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> ComponentAdapter + fn c_element(self) -> ComponentAdapter where Self: Sized, { ComponentAdapter::new(self) } - fn styleable(self) -> StylableComponentAdapter + fn c_styleable(self) -> StylableComponentAdapter where Self: Sized, { @@ -65,7 +110,7 @@ impl Component for C { } } -// StylableComponent -> GeneralComponent +// StylableComponent -> Component pub struct StylableComponentAdapter, V: View> { component: C, phantom: std::marker::PhantomData, @@ -80,16 +125,40 @@ impl, V: View> StylableComponentAdapter { } } -impl StyleableComponent for StylableComponentAdapter { +impl, V: View> StyleableComponent for StylableComponentAdapter { type Style = (); type Output = C; - fn with_style(self, _: Self::Style) -> Self::Output { + fn c_with_style(self, _: Self::Style) -> Self::Output { self.component } } +// Element -> GeneralComponent + +pub struct DynamicElementAdapter { + element: Box, +} + +impl DynamicElementAdapter { + pub fn new(element: AnyElement) -> Self { + DynamicElementAdapter { + element: Box::new(element) as Box, + } + } +} + +impl GeneralComponent for DynamicElementAdapter { + fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { + let element = self + .element + .downcast::>() + .expect("Don't move elements out of their view :("); + *element + } +} + // Element -> Component pub struct ElementAdapter { element: AnyElement, diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 2dc45e397394b84671533471e7c93937d9469354..b3167efe507f6ae36fb870412793d38d39ae4e20 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, StyleableComponent, TooltipStyle}, + elements::{Component, GeneralStyleableComponent, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; @@ -96,7 +96,7 @@ impl SearchOptions { .with_contents(Svg::new(self.icon())) .toggleable(active) .with_style(button_style) - .element() + .c_element() .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 1e395405cbb506596248d51ac5b387ca2a654e83..3679b1fa4f1c1313d4fa8340d518d9949c5d0a31 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,4 +1,4 @@ -use gpui::elements::StyleableComponent; +use gpui::elements::GeneralStyleableComponent; use crate::{Interactive, Toggleable}; @@ -6,18 +6,18 @@ use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; pub type ToggleIconButtonStyle = Toggleable>>; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } } pub mod toggle { - use gpui::elements::{GeneralComponent, StyleableComponent}; + use gpui::elements::{GeneralComponent, GeneralStyleableComponent}; use crate::Toggleable; @@ -27,7 +27,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -37,7 +37,7 @@ pub mod toggle { } } - impl StyleableComponent for Toggle { + impl GeneralStyleableComponent for Toggle { type Style = Toggleable; type Output = Toggle; @@ -51,7 +51,7 @@ pub mod toggle { } } - impl GeneralComponent for Toggle> { + impl GeneralComponent for Toggle> { fn render( self, v: &mut V, @@ -69,7 +69,8 @@ pub mod action_button { use gpui::{ elements::{ - ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle, + ContainerStyle, GeneralComponent, GeneralStyleableComponent, MouseEventHandler, + TooltipStyle, }, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, @@ -121,7 +122,10 @@ pub mod action_button { self } - pub fn with_contents(self, contents: C) -> ActionButton { + pub fn with_contents( + self, + contents: C, + ) -> ActionButton { ActionButton { action: self.action, tag: self.tag, @@ -132,7 +136,7 @@ pub mod action_button { } } - impl StyleableComponent for ActionButton { + impl GeneralStyleableComponent for ActionButton { type Style = Interactive>; type Output = ActionButton>; @@ -148,7 +152,7 @@ pub mod action_button { } } - impl GeneralComponent for ActionButton> { + impl GeneralComponent for ActionButton> { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let mut button = MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { let style = self.style.style_for(state); @@ -195,7 +199,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, StyleableComponent}, + elements::{GeneralComponent, GeneralStyleableComponent}, Element, }; use schemars::JsonSchema; @@ -261,7 +265,7 @@ pub mod svg { } } - impl StyleableComponent for Svg<()> { + impl GeneralStyleableComponent for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -294,7 +298,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, LabelStyle, StyleableComponent}, + elements::{GeneralComponent, GeneralStyleableComponent, LabelStyle}, Element, }; @@ -312,7 +316,7 @@ pub mod label { } } - impl StyleableComponent for Label<()> { + impl GeneralStyleableComponent for Label<()> { type Style = LabelStyle; type Output = Label; From bd3ab82dac7d59f19a104416b29976d070946004 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 05:18:53 -0700 Subject: [PATCH 003/142] Add disclosable components into channels Rename components to more closely match their purpose --- crates/client/src/channel_store.rs | 10 ++ crates/collab_ui/src/collab_panel.rs | 170 +++++++----------- crates/gpui/examples/components.rs | 14 +- crates/gpui/src/elements.rs | 11 +- crates/gpui/src/elements/component.rs | 66 +++---- crates/gpui/src/elements/container.rs | 8 + crates/search/src/search.rs | 4 +- crates/theme/src/components.rs | 245 +++++++++++++++++++++----- crates/theme/src/theme.rs | 21 ++- styles/src/style_tree/collab_panel.ts | 4 + 10 files changed, 359 insertions(+), 194 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 03d334a9defcc05255874c75349f894d1b9bc1f7..6352ac791edaa0bf50704e4e42bf41069031be55 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -114,6 +114,16 @@ impl ChannelStore { } } + pub fn has_children(&self, channel_id: ChannelId) -> bool { + self.channel_paths.iter().any(|path| { + if let Some(ix) = path.iter().position(|id| *id == channel_id) { + path.len() > ix + 1 + } else { + false + } + }) + } + pub fn channel_count(&self) -> usize { self.channel_paths.len() } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f35c07d5aa0a9e98a8a24fee93c895308f7a09bb..41c87094d2c27c561c1c0ddb5548f461cdfdb511 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -16,9 +16,9 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Empty, Flex, GeneralComponent, GeneralStyleableComponent, Image, Label, - List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, - ParentElement, Stack, Svg, + Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, + StyleableComponent, Svg, }, geometry::{ rect::RectF, @@ -36,7 +36,7 @@ use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; use staff_mode::StaffMode; use std::{borrow::Cow, mem, sync::Arc}; -use theme::IconButton; +use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -44,10 +44,7 @@ use workspace::{ Workspace, }; -use crate::{ - collab_panel::components::{DisclosureExt, DisclosureStyle}, - face_pile::FacePile, -}; +use crate::face_pile::FacePile; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -57,6 +54,11 @@ struct RemoveChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct ToggleCollapsed { + channel_id: u64, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct NewChannel { channel_id: u64, @@ -86,7 +88,8 @@ impl_actions!( NewChannel, InviteMembers, ManageMembers, - RenameChannel + RenameChannel, + ToggleCollapsed ] ); @@ -109,6 +112,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::manage_members); cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); + cx.add_action(CollabPanel::toggle_channel_collapsed); } #[derive(Debug)] @@ -151,6 +155,7 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, + collapsed_channels: Vec, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -402,6 +407,7 @@ impl CollabPanel { subscriptions: Vec::default(), match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], + collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, @@ -661,10 +667,24 @@ impl CollabPanel { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } + let mut collapse_depth = None; for mat in matches { let (depth, channel) = channel_store.channel_at_index(mat.candidate_id).unwrap(); + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else if let Some(collapsed_depth) = collapse_depth { + if depth > collapsed_depth { + continue; + } + if self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else { + collapse_depth = None; + } + } + match &self.channel_editing_state { Some(ChannelEditingState::Create { parent_id, .. }) if *parent_id == Some(channel.id) => @@ -1483,6 +1503,11 @@ impl CollabPanel { cx: &AppContext, ) -> AnyElement { Flex::row() + .with_child( + Empty::new() + .constrained() + .with_width(theme.collab_panel.disclosure.button_space()), + ) .with_child( Svg::new("icons/hash.svg") .with_color(theme.collab_panel.channel_hash.color) @@ -1541,6 +1566,10 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; + let has_children = self.channel_store.read(cx).has_children(channel_id); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok()); + let is_active = iife!({ let call_channel = ActiveCall::global(cx) .read(cx) @@ -1554,7 +1583,7 @@ impl CollabPanel { const FACEPILE_LIMIT: usize = 3; MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { - Flex::row() + Flex::::row() .with_child( Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) @@ -1603,6 +1632,14 @@ impl CollabPanel { } }) .align_children_center() + .styleable_component() + .disclosable( + disclosed, + Box::new(ToggleCollapsed { channel_id }), + channel_id as usize, + ) + .with_style(theme.disclosure.clone()) + .element() .constrained() .with_height(theme.row_height) .contained() @@ -1619,17 +1656,6 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) - .dynamic_component() - .stylable() - .disclosable(true, Box::new(RemoveChannel { channel_id: 0 })) - .with_style({ - fn style() -> DisclosureStyle<()> { - todo!() - } - - style() - }) - .element() .into_any() } @@ -2024,6 +2050,24 @@ impl CollabPanel { self.update_entries(false, cx); } + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapsed, cx: &mut ViewContext) { + let channel_id = action.channel_id; + match self.collapsed_channels.binary_search(&channel_id) { + Ok(ix) => { + self.collapsed_channels.remove(ix); + } + Err(ix) => { + self.collapsed_channels.insert(ix, channel_id); + } + }; + self.update_entries(false, cx); + cx.notify(); + } + + fn is_channel_collapsed(&self, channel: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel).is_ok() + } + fn leave_call(cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) @@ -2537,87 +2581,3 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .contained() .with_style(style.container) } - -mod components { - - use gpui::{ - elements::{Empty, Flex, GeneralComponent, GeneralStyleableComponent, ParentElement}, - Action, Element, - }; - use theme::components::{ - action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, - }; - - #[derive(Clone)] - pub struct DisclosureStyle { - disclosure: ToggleIconButtonStyle, - spacing: f32, - content: S, - } - - pub struct Disclosable { - disclosed: bool, - action: Box, - content: C, - style: S, - } - - impl Disclosable<(), ()> { - fn new(disclosed: bool, content: C, action: Box) -> Disclosable { - Disclosable { - disclosed, - content, - action, - style: (), - } - } - } - - impl GeneralStyleableComponent for Disclosable { - type Style = DisclosureStyle; - - type Output = Disclosable; - - fn with_style(self, style: Self::Style) -> Self::Output { - Disclosable { - disclosed: self.disclosed, - action: self.action, - content: self.content, - style, - } - } - } - - impl GeneralComponent for Disclosable> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { - Flex::row() - .with_child( - ActionButton::new_dynamic(self.action) - .with_contents(Svg::new("path")) - .toggleable(self.disclosed) - .with_style(self.style.disclosure) - .element(), - ) - .with_child(Empty::new().constrained().with_width(self.style.spacing)) - .with_child(self.content.with_style(self.style.content).render(v, cx)) - .align_children_center() - .into_any() - } - } - - pub trait DisclosureExt { - fn disclosable(self, disclosed: bool, action: Box) -> Disclosable - where - Self: Sized; - } - - impl DisclosureExt for C { - fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { - Disclosable::new(disclosed, self, action) - } - } -} diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index aeeab8101d1e926f287e72c9fd22a44af8d645df..5aa648bd6573e6e2cec46a06eced88fe08effd94 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -2,7 +2,7 @@ use button_component::Button; use gpui::{ color::Color, - elements::{Component, ContainerStyle, Flex, Label, ParentElement}, + elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent}, fonts::{self, TextStyle}, platform::WindowOptions, AnyElement, App, Element, Entity, View, ViewContext, @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .c_element(), + .stateful_element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .c_element(), + .stateful_element(), ) .expanded() .contained() @@ -114,7 +114,7 @@ mod theme { // Component creation: mod toggleable_button { use gpui::{ - elements::{Component, ContainerStyle, LabelStyle}, + elements::{ContainerStyle, LabelStyle, StatefulComponent}, scene::MouseClick, EventContext, View, }; @@ -156,7 +156,7 @@ mod toggleable_button { } } - impl Component for ToggleableButton { + impl StatefulComponent 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)) @@ -171,7 +171,7 @@ mod toggleable_button { mod button_component { use gpui::{ - elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler}, + elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent}, platform::MouseButton, scene::MouseClick, AnyElement, Element, EventContext, TypeTag, View, ViewContext, @@ -212,7 +212,7 @@ mod button_component { } } - impl Component for Button { + impl StatefulComponent for Button { fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { let click_handler = self.click_handler; diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index c0484ef9395a9e25e82384b30aa5fdc88d81bf07..4fcf3815f58f328ff564f9e893b3c14ac81a4b6a 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -230,19 +230,26 @@ pub trait Element: 'static { MouseEventHandler::for_child::(self.into_any(), region_id) } - fn component(self) -> ElementAdapter + fn stateful_component(self) -> ElementAdapter where Self: Sized, { ElementAdapter::new(self.into_any()) } - fn dynamic_component(self) -> DynamicElementAdapter + fn component(self) -> DynamicElementAdapter where Self: Sized, { DynamicElementAdapter::new(self.into_any()) } + + fn styleable_component(self) -> StylableAdapter + where + Self: Sized, + { + DynamicElementAdapter::new(self.into_any()).stylable() + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index f5a4180d7f379c059a175c731311ca46360f0132..703d6eca075ee59e04409c14c9a58fd2f64bc35a 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -9,35 +9,35 @@ use crate::{ use super::Empty; -pub trait GeneralComponent { +pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> ComponentAdapter + fn element(self) -> StatefulAdapter where Self: Sized, { - ComponentAdapter::new(self) + StatefulAdapter::new(self) } - fn stylable(self) -> GeneralStylableComponentAdapter + fn stylable(self) -> StylableAdapter where Self: Sized, { - GeneralStylableComponentAdapter::new(self) + StylableAdapter::new(self) } } -pub struct GeneralStylableComponentAdapter { +pub struct StylableAdapter { component: C, } -impl GeneralStylableComponentAdapter { +impl StylableAdapter { pub fn new(component: C) -> Self { Self { component } } } -impl GeneralStyleableComponent for GeneralStylableComponentAdapter { +impl StyleableComponent for StylableAdapter { type Style = (); type Output = C; @@ -47,20 +47,20 @@ impl GeneralStyleableComponent for GeneralStylableComponent } } -pub trait GeneralStyleableComponent { +pub trait StyleableComponent { type Style: Clone; - type Output: GeneralComponent; + type Output: Component; fn with_style(self, style: Self::Style) -> Self::Output; } -impl GeneralComponent for () { +impl Component for () { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { Empty::new().into_any() } } -impl GeneralStyleableComponent for () { +impl StyleableComponent for () { type Style = (); type Output = (); @@ -69,54 +69,54 @@ impl GeneralStyleableComponent for () { } } -pub trait StyleableComponent { +pub trait StatefulStyleableComponent { type Style: Clone; - type Output: Component; + type Output: StatefulComponent; - fn c_with_style(self, style: Self::Style) -> Self::Output; + fn stateful_with_style(self, style: Self::Style) -> Self::Output; } -impl StyleableComponent for C { +impl StatefulStyleableComponent for C { type Style = C::Style; type Output = C::Output; - fn c_with_style(self, style: Self::Style) -> Self::Output { + fn stateful_with_style(self, style: Self::Style) -> Self::Output { self.with_style(style) } } -pub trait Component { +pub trait StatefulComponent { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn c_element(self) -> ComponentAdapter + fn stateful_element(self) -> StatefulAdapter where Self: Sized, { - ComponentAdapter::new(self) + StatefulAdapter::new(self) } - fn c_styleable(self) -> StylableComponentAdapter + fn stateful_styleable(self) -> StatefulStylableAdapter where Self: Sized, { - StylableComponentAdapter::new(self) + StatefulStylableAdapter::new(self) } } -impl Component for C { +impl StatefulComponent for C { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { self.render(v, cx) } } // StylableComponent -> Component -pub struct StylableComponentAdapter, V: View> { +pub struct StatefulStylableAdapter, V: View> { component: C, phantom: std::marker::PhantomData, } -impl, V: View> StylableComponentAdapter { +impl, V: View> StatefulStylableAdapter { pub fn new(component: C) -> Self { Self { component, @@ -125,12 +125,14 @@ impl, V: View> StylableComponentAdapter { } } -impl, V: View> StyleableComponent for StylableComponentAdapter { +impl, V: View> StatefulStyleableComponent + for StatefulStylableAdapter +{ type Style = (); type Output = C; - fn c_with_style(self, _: Self::Style) -> Self::Output { + fn stateful_with_style(self, _: Self::Style) -> Self::Output { self.component } } @@ -149,7 +151,7 @@ impl DynamicElementAdapter { } } -impl GeneralComponent for DynamicElementAdapter { +impl Component for DynamicElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { let element = self .element @@ -174,20 +176,20 @@ impl ElementAdapter { } } -impl Component for ElementAdapter { +impl StatefulComponent for ElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { self.element } } // Component -> Element -pub struct ComponentAdapter { +pub struct StatefulAdapter { component: Option, element: Option>, phantom: PhantomData, } -impl ComponentAdapter { +impl StatefulAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), @@ -197,7 +199,7 @@ impl ComponentAdapter { } } -impl + 'static> Element for ComponentAdapter { +impl + 'static> Element for StatefulAdapter { type LayoutState = (); type PaintState = (); diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index bb1366b4e767d72b32cd15ad49e8e429cab4fc7b..68d3e57610587d9984d36e8202eca343b6ba9c9f 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -45,6 +45,14 @@ impl ContainerStyle { ..Default::default() } } + + pub fn additional_length(&self) -> f32 { + self.padding.left + + self.padding.right + + self.border.width * 2. + + self.margin.left + + self.margin.right + } } pub struct Container { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index b3167efe507f6ae36fb870412793d38d39ae4e20..2dc45e397394b84671533471e7c93937d9469354 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, GeneralStyleableComponent, TooltipStyle}, + elements::{Component, StyleableComponent, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; @@ -96,7 +96,7 @@ impl SearchOptions { .with_contents(Svg::new(self.icon())) .toggleable(active) .with_style(button_style) - .c_element() + .element() .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 3679b1fa4f1c1313d4fa8340d518d9949c5d0a31..6f9242199f6037e4f504f7cc0cf5d92707249a41 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,23 +1,147 @@ -use gpui::elements::GeneralStyleableComponent; +use gpui::{elements::StyleableComponent, Action}; use crate::{Interactive, Toggleable}; -use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; +use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; pub type ToggleIconButtonStyle = Toggleable>>; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; + fn disclosable( + self, + disclosed: Option, + action: Box, + id: usize, + ) -> Disclosable; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } + + /// Some(True) => disclosed => content is visible + /// Some(false) => closed => content is hidden + /// None => No disclosure button, but reserve spacing + fn disclosable( + self, + disclosed: Option, + action: Box, + id: usize, + ) -> Disclosable { + Disclosable::new(disclosed, self, action, id) + } +} + +pub mod disclosure { + + use gpui::{ + elements::{Component, Empty, Flex, ParentElement, StyleableComponent}, + Action, Element, + }; + use schemars::JsonSchema; + use serde_derive::Deserialize; + + use super::{action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + + #[derive(Clone, Default, Deserialize, JsonSchema)] + pub struct DisclosureStyle { + pub button: ToggleIconButtonStyle, + pub spacing: f32, + #[serde(flatten)] + content: S, + } + + impl DisclosureStyle { + pub fn button_space(&self) -> f32 { + self.spacing + self.button.button_width.unwrap() + } + } + + pub struct Disclosable { + disclosed: Option, + action: Box, + id: usize, + content: C, + style: S, + } + + impl Disclosable<(), ()> { + pub fn new( + disclosed: Option, + content: C, + action: Box, + id: usize, + ) -> Disclosable { + Disclosable { + disclosed, + content, + action, + id, + style: (), + } + } + } + + impl StyleableComponent for Disclosable { + type Style = DisclosureStyle; + + type Output = Disclosable; + + fn with_style(self, style: Self::Style) -> Self::Output { + Disclosable { + disclosed: self.disclosed, + action: self.action, + content: self.content, + id: self.id, + style, + } + } + } + + impl Component for Disclosable> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child(if let Some(disclosed) = self.disclosed { + ActionButton::new_dynamic(self.action) + .with_id(self.id) + .with_contents(Svg::new(if disclosed { + "icons/file_icons/chevron_down.svg" + } else { + "icons/file_icons/chevron_right.svg" + })) + .toggleable(disclosed) + .with_style(self.style.button) + .element() + .into_any() + } else { + Empty::new() + .into_any() + .constrained() + // TODO: Why is this optional at all? + .with_width(self.style.button.button_width.unwrap()) + .into_any() + }) + .with_child(Empty::new().constrained().with_width(self.style.spacing)) + .with_child( + self.content + .with_style(self.style.content) + .render(v, cx) + .flex(1., true), + ) + .align_children_center() + .into_any() + } + } } pub mod toggle { - use gpui::elements::{GeneralComponent, GeneralStyleableComponent}; + use gpui::elements::{Component, StyleableComponent}; use crate::Toggleable; @@ -27,7 +151,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -37,7 +161,7 @@ pub mod toggle { } } - impl GeneralStyleableComponent for Toggle { + impl StyleableComponent for Toggle { type Style = Toggleable; type Output = Toggle; @@ -51,7 +175,7 @@ pub mod toggle { } } - impl GeneralComponent for Toggle> { + impl Component for Toggle> { fn render( self, v: &mut V, @@ -69,8 +193,7 @@ pub mod action_button { use gpui::{ elements::{ - ContainerStyle, GeneralComponent, GeneralStyleableComponent, MouseEventHandler, - TooltipStyle, + Component, ContainerStyle, MouseEventHandler, StyleableComponent, TooltipStyle, }, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, @@ -80,24 +203,28 @@ pub mod action_button { use crate::Interactive; + #[derive(Clone, Deserialize, Default, JsonSchema)] + pub struct ButtonStyle { + #[serde(flatten)] + pub container: ContainerStyle, + // TODO: These are incorrect for the intended usage of the buttons. + // The size should be constant, but putting them here duplicates them + // across the states the buttons can be in + pub button_width: Option, + pub button_height: Option, + #[serde(flatten)] + contents: C, + } + pub struct ActionButton { action: Box, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, + id: usize, contents: C, style: Interactive, } - #[derive(Clone, Deserialize, Default, JsonSchema)] - pub struct ButtonStyle { - #[serde(flatten)] - container: ContainerStyle, - button_width: Option, - button_height: Option, - #[serde(flatten)] - contents: C, - } - impl ActionButton<(), ()> { pub fn new_dynamic(action: Box) -> Self { Self { @@ -105,6 +232,7 @@ pub mod action_button { tag: action.type_tag(), style: Interactive::new_blank(), tooltip: None, + id: 0, action, } } @@ -122,21 +250,24 @@ pub mod action_button { self } - pub fn with_contents( - self, - contents: C, - ) -> ActionButton { + pub fn with_id(mut self, id: usize) -> Self { + self.id = id; + self + } + + pub fn with_contents(self, contents: C) -> ActionButton { ActionButton { action: self.action, tag: self.tag, style: self.style, tooltip: self.tooltip, + id: self.id, contents, } } } - impl GeneralStyleableComponent for ActionButton { + impl StyleableComponent for ActionButton { type Style = Interactive>; type Output = ActionButton>; @@ -146,15 +277,15 @@ pub mod action_button { tag: self.tag, contents: self.contents, tooltip: self.tooltip, - + id: self.id, style, } } } - impl GeneralComponent for ActionButton> { + impl Component for ActionButton> { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - let mut button = MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { + let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents @@ -177,8 +308,13 @@ pub mod action_button { .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); move |_, _, cx| { - cx.window() - .dispatch_action(cx.view_id(), action.as_ref(), cx); + let window = cx.window(); + let view = cx.view_id(); + let action = action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + window.dispatch_action(view, action.as_ref(), &mut cx) + }) + .detach(); } }) .with_cursor_style(CursorStyle::PointingHand) @@ -199,7 +335,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, GeneralStyleableComponent}, + elements::{Component, Empty, StyleableComponent}, Element, }; use schemars::JsonSchema; @@ -222,6 +358,7 @@ pub mod svg { pub enum IconSize { IconSize { icon_size: f32 }, Dimensions { width: f32, height: f32 }, + IconDimensions { icon_width: f32, icon_height: f32 }, } #[derive(Deserialize)] @@ -245,6 +382,14 @@ pub mod svg { icon_height: height, color, }, + IconSize::IconDimensions { + icon_width, + icon_height, + } => SvgStyle { + icon_width, + icon_height, + color, + }, }; Ok(result) @@ -252,20 +397,27 @@ pub mod svg { } pub struct Svg { - path: Cow<'static, str>, + path: Option>, style: S, } impl Svg<()> { pub fn new(path: impl Into>) -> Self { Self { - path: path.into(), + path: Some(path.into()), + style: (), + } + } + + pub fn optional(path: Option>>) -> Self { + Self { + path: path.map(Into::into), style: (), } } } - impl GeneralStyleableComponent for Svg<()> { + impl StyleableComponent for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -278,18 +430,23 @@ pub mod svg { } } - impl GeneralComponent for Svg { + impl Component for Svg { fn render( self, _: &mut V, _: &mut gpui::ViewContext, ) -> gpui::AnyElement { - gpui::elements::Svg::new(self.path) - .with_color(self.style.color) - .constrained() - .with_width(self.style.icon_width) - .with_height(self.style.icon_height) - .into_any() + if let Some(path) = self.path { + gpui::elements::Svg::new(path) + .with_color(self.style.color) + .constrained() + } else { + Empty::new().constrained() + } + .constrained() + .with_width(self.style.icon_width) + .with_height(self.style.icon_height) + .into_any() } } } @@ -298,7 +455,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, GeneralStyleableComponent, LabelStyle}, + elements::{Component, LabelStyle, StyleableComponent}, Element, }; @@ -316,7 +473,7 @@ pub mod label { } } - impl GeneralStyleableComponent for Label<()> { + impl StyleableComponent for Label<()> { type Style = LabelStyle; type Output = Label; @@ -329,7 +486,7 @@ pub mod label { } } - impl GeneralComponent for Label { + impl Component for Label { fn render( self, _: &mut V, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 80e823632a9c10613abbf1f3f5effee615c1cd15..08da9af29912a794cf6893911416b0e2525e8d8b 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,7 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::ToggleIconButtonStyle; +use components::{disclosure::DisclosureStyle, ToggleIconButtonStyle}; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -14,7 +14,7 @@ use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use settings::SettingsStore; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle}; pub use theme_registry::*; @@ -221,6 +221,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, + pub disclosure: DisclosureStyle<()>, pub list_empty_state: Toggleable>, pub list_empty_icon: Icon, pub list_empty_label_container: ContainerStyle, @@ -890,6 +891,14 @@ pub struct Interactive { pub disabled: Option, } +impl Deref for Interactive { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.default + } +} + impl Interactive<()> { pub fn new_blank() -> Self { Self { @@ -907,6 +916,14 @@ pub struct Toggleable { inactive: T, } +impl Deref for Toggleable { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inactive + } +} + impl Toggleable<()> { pub fn new_blank() -> Self { Self { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index a102ee769194d4b4e0189f5b2541de53c3408180..5242f90c8d83fcdbf842c453f468253ec4eb0f6b 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -152,6 +152,10 @@ export default function contacts_panel(): any { return { ...collab_modals(), + disclosure: { + button: toggleable_icon_button(theme, {}), + spacing: 4, + }, log_in_button: interactive({ base: { background: background(theme.middle), From e946b0a2ece63ba160804b5260a4b00433e3130e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 14:40:05 -0700 Subject: [PATCH 004/142] Finish building out adapters and names Document core traits Add start for a component storybook --- Cargo.lock | 11 ++ Cargo.toml | 1 + crates/collab_ui/src/collab_panel.rs | 11 +- crates/gpui/examples/components.rs | 4 +- crates/gpui/src/elements.rs | 12 +- crates/gpui/src/elements/component.rs | 192 ++++++++++++++++++-------- crates/search/src/search.rs | 8 +- crates/theme/src/components.rs | 117 +++++++--------- 8 files changed, 208 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aab5504a86b61ae6b7c2c2c7591cc0c829ead3d4..01c74fb4ceb62f59d322df7ba8355c3c1414de52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1562,6 +1562,17 @@ dependencies = [ "workspace", ] +[[package]] +name = "component_test" +version = "0.1.0" +dependencies = [ + "gpui", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "concurrent-queue" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 7ea79138c026dc6e1bbef10b22fd925ff93f0f1b..d434f347734bfa2922eebe261031c710a6e10812 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/component_test", "crates/context_menu", "crates/copilot", "crates/copilot_button", diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 41c87094d2c27c561c1c0ddb5548f461cdfdb511..52711281c77f9eb6a92b64855d90a25c50615361 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -17,8 +17,8 @@ use gpui::{ actions, elements::{ Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, - StyleableComponent, Svg, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, + Stack, Svg, }, geometry::{ rect::RectF, @@ -1633,11 +1633,8 @@ impl CollabPanel { }) .align_children_center() .styleable_component() - .disclosable( - disclosed, - Box::new(ToggleCollapsed { channel_id }), - channel_id as usize, - ) + .disclosable(disclosed, Box::new(ToggleCollapsed { channel_id })) + .with_id(channel_id as usize) .with_style(theme.disclosure.clone()) .element() .constrained() diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index 5aa648bd6573e6e2cec46a06eced88fe08effd94..d3ca0d1eccaef343058968856a0e8715849357a8 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .stateful_element(), + .element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .stateful_element(), + .element(), ) .expanded() .contained() diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 4fcf3815f58f328ff564f9e893b3c14ac81a4b6a..3c80ef73fe83d0d953c368c6f45a322ee47f75fb 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -230,25 +230,25 @@ pub trait Element: 'static { MouseEventHandler::for_child::(self.into_any(), region_id) } - fn stateful_component(self) -> ElementAdapter + fn component(self) -> StatelessElementAdapter where Self: Sized, { - ElementAdapter::new(self.into_any()) + StatelessElementAdapter::new(self.into_any()) } - fn component(self) -> DynamicElementAdapter + fn stateful_component(self) -> StatefulElementAdapter where Self: Sized, { - DynamicElementAdapter::new(self.into_any()) + StatefulElementAdapter::new(self.into_any()) } - fn styleable_component(self) -> StylableAdapter + fn styleable_component(self) -> StylableAdapter where Self: Sized, { - DynamicElementAdapter::new(self.into_any()).stylable() + StatelessElementAdapter::new(self.into_any()).stylable() } } diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 703d6eca075ee59e04409c14c9a58fd2f64bc35a..e391ff9b057edb6e8740d2ec7963bcc45421d67a 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -9,14 +9,15 @@ use crate::{ use super::Empty; +/// The core stateless component trait, simply rendering an element tree pub trait Component { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + fn render(self, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> StatefulAdapter + fn element(self) -> ComponentAdapter where Self: Sized, { - StatefulAdapter::new(self) + ComponentAdapter::new(self) } fn stylable(self) -> StylableAdapter @@ -25,8 +26,46 @@ pub trait Component { { StylableAdapter::new(self) } + + fn stateful(self) -> StatefulAdapter + where + Self: Sized, + { + StatefulAdapter::new(self) + } +} + +/// Allows a a component's styles to be rebound in a simple way. +pub trait Stylable: Component { + type Style: Clone; + + fn with_style(self, style: Self::Style) -> Self; } +/// This trait models the typestate pattern for a component's style, +/// enforcing at compile time that a component is only usable after +/// it has been styled while still allowing for late binding of the +/// styling information +pub trait SafeStylable { + type Style: Clone; + type Output: Component; + + fn with_style(self, style: Self::Style) -> Self::Output; +} + +/// All stylable components can trivially implement SafeStylable +impl SafeStylable for C { + type Style = C::Style; + + type Output = C; + + fn with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + +/// Allows converting an unstylable component into a stylable one +/// by using `()` as the style type pub struct StylableAdapter { component: C, } @@ -37,7 +76,7 @@ impl StylableAdapter { } } -impl StyleableComponent for StylableAdapter { +impl SafeStylable for StylableAdapter { type Style = (); type Output = C; @@ -47,36 +86,61 @@ impl StyleableComponent for StylableAdapter { } } -pub trait StyleableComponent { - type Style: Clone; - type Output: Component; +/// This is a secondary trait for components that can be styled +/// which rely on their view's state. This is useful for components that, for example, +/// want to take click handler callbacks Unfortunately, the generic bound on the +/// Component trait makes it incompatible with the stateless components above. +// So let's just replicate them for now +pub trait StatefulComponent { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn with_style(self, style: Self::Style) -> Self::Output; + fn element(self) -> ComponentAdapter + where + Self: Sized, + { + ComponentAdapter::new(self) + } + + fn styleable(self) -> StatefulStylableAdapter + where + Self: Sized, + { + StatefulStylableAdapter::new(self) + } + + fn stateless(self) -> StatelessElementAdapter + where + Self: Sized + 'static, + { + StatelessElementAdapter::new(self.element().into_any()) + } } -impl Component for () { - fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { - Empty::new().into_any() +/// It is trivial to convert stateless components to stateful components, so lets +/// do so en masse. Note that the reverse is impossible without a helper. +impl StatefulComponent for C { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + self.render(cx) } } -impl StyleableComponent for () { - type Style = (); - type Output = (); +/// Same as stylable, but generic over a view type +pub trait StatefulStylable: StatefulComponent { + type Style: Clone; - fn with_style(self, _: Self::Style) -> Self::Output { - () - } + fn stateful_with_style(self, style: Self::Style) -> Self; } -pub trait StatefulStyleableComponent { +/// Same as SafeStylable, but generic over a view type +pub trait StatefulSafeStylable { type Style: Clone; type Output: StatefulComponent; fn stateful_with_style(self, style: Self::Style) -> Self::Output; } -impl StatefulStyleableComponent for C { +/// Converting from stateless to stateful +impl StatefulSafeStylable for C { type Style = C::Style; type Output = C::Output; @@ -86,31 +150,29 @@ impl StatefulStyleableComponent for C { } } -pub trait StatefulComponent { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - - fn stateful_element(self) -> StatefulAdapter - where - Self: Sized, - { - StatefulAdapter::new(self) - } +// A helper for converting stateless components into stateful ones +pub struct StatefulAdapter { + component: C, + phantom: std::marker::PhantomData, +} - fn stateful_styleable(self) -> StatefulStylableAdapter - where - Self: Sized, - { - StatefulStylableAdapter::new(self) +impl StatefulAdapter { + pub fn new(component: C) -> Self { + Self { + component, + phantom: std::marker::PhantomData, + } } } -impl StatefulComponent for C { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { - self.render(v, cx) +impl StatefulComponent for StatefulAdapter { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + self.component.render(cx) } } -// StylableComponent -> Component +// A helper for converting stateful but style-less components into stylable ones +// by using `()` as the style type pub struct StatefulStylableAdapter, V: View> { component: C, phantom: std::marker::PhantomData, @@ -125,9 +187,7 @@ impl, V: View> StatefulStylableAdapter { } } -impl, V: View> StatefulStyleableComponent - for StatefulStylableAdapter -{ +impl, V: View> StatefulSafeStylable for StatefulStylableAdapter { type Style = (); type Output = C; @@ -137,37 +197,37 @@ impl, V: View> StatefulStyleableComponent } } -// Element -> GeneralComponent - -pub struct DynamicElementAdapter { +/// A way of erasing the view generic from an element, useful +/// for wrapping up an explicit element tree into stateless +/// components +pub struct StatelessElementAdapter { element: Box, } -impl DynamicElementAdapter { +impl StatelessElementAdapter { pub fn new(element: AnyElement) -> Self { - DynamicElementAdapter { + StatelessElementAdapter { element: Box::new(element) as Box, } } } -impl Component for DynamicElementAdapter { - fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { - let element = self +impl Component for StatelessElementAdapter { + fn render(self, _: &mut ViewContext) -> AnyElement { + *self .element .downcast::>() - .expect("Don't move elements out of their view :("); - *element + .expect("Don't move elements out of their view :(") } } -// Element -> Component -pub struct ElementAdapter { +// For converting elements into stateful components +pub struct StatefulElementAdapter { element: AnyElement, _phantom: std::marker::PhantomData, } -impl ElementAdapter { +impl StatefulElementAdapter { pub fn new(element: AnyElement) -> Self { Self { element, @@ -176,20 +236,35 @@ impl ElementAdapter { } } -impl StatefulComponent for ElementAdapter { +impl StatefulComponent for StatefulElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { self.element } } -// Component -> Element -pub struct StatefulAdapter { +/// A convenient shorthand for creating an empty component. +impl Component for () { + fn render(self, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } +} + +impl Stylable for () { + type Style = (); + + fn with_style(self, _: Self::Style) -> Self { + () + } +} + +// For converting components back into Elements +pub struct ComponentAdapter { component: Option, element: Option>, phantom: PhantomData, } -impl StatefulAdapter { +impl ComponentAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), @@ -199,7 +274,7 @@ impl StatefulAdapter { } } -impl + 'static> Element for StatefulAdapter { +impl + 'static> Element for ComponentAdapter { type LayoutState = (); type PaintState = (); @@ -262,6 +337,7 @@ impl + 'static> Element for StatefulAdapter< ) -> serde_json::Value { serde_json::json!({ "type": "ComponentAdapter", + "component": std::any::type_name::(), "child": self.element.as_ref().map(|el| el.debug(view, cx)), }) } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 2dc45e397394b84671533471e7c93937d9469354..47f7f485c486a86d3ff3725b5e05aedb52305b36 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,15 +2,13 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, StyleableComponent, TooltipStyle}, + elements::{Component, SafeStylable, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{ - action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, -}; +use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; pub mod buffer_search; mod history; @@ -91,7 +89,7 @@ impl SearchOptions { tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { - ActionButton::new_dynamic(self.to_toggle_action()) + Button::dynamic_action(self.to_toggle_action()) .with_tooltip(format!("Toggle {}", self.label()), tooltip_style) .with_contents(Svg::new(self.icon())) .toggleable(active) diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 6f9242199f6037e4f504f7cc0cf5d92707249a41..d9a857915c53f0f70dcb8849abbc3fea538551af 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,4 +1,4 @@ -use gpui::{elements::StyleableComponent, Action}; +use gpui::{elements::SafeStylable, Action}; use crate::{Interactive, Toggleable}; @@ -6,44 +6,34 @@ use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, t pub type ToggleIconButtonStyle = Toggleable>>; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; - fn disclosable( - self, - disclosed: Option, - action: Box, - id: usize, - ) -> Disclosable; + fn disclosable(self, disclosed: Option, action: Box) -> Disclosable; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } /// Some(True) => disclosed => content is visible /// Some(false) => closed => content is hidden - /// None => No disclosure button, but reserve spacing - fn disclosable( - self, - disclosed: Option, - action: Box, - id: usize, - ) -> Disclosable { - Disclosable::new(disclosed, self, action, id) + /// None => No disclosure button, but reserve disclosure spacing + fn disclosable(self, disclosed: Option, action: Box) -> Disclosable { + Disclosable::new(disclosed, self, action) } } pub mod disclosure { use gpui::{ - elements::{Component, Empty, Flex, ParentElement, StyleableComponent}, + elements::{Component, Empty, Flex, ParentElement, SafeStylable}, Action, Element, }; use schemars::JsonSchema; use serde_derive::Deserialize; - use super::{action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + use super::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { @@ -72,19 +62,24 @@ pub mod disclosure { disclosed: Option, content: C, action: Box, - id: usize, ) -> Disclosable { Disclosable { disclosed, content, action, - id, + id: 0, style: (), } } } - impl StyleableComponent for Disclosable { + impl Disclosable { + pub fn with_id(self, id: usize) -> Disclosable { + Disclosable { id, ..self } + } + } + + impl SafeStylable for Disclosable { type Style = DisclosureStyle; type Output = Disclosable; @@ -100,15 +95,11 @@ pub mod disclosure { } } - impl Component for Disclosable> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for Disclosable> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { Flex::row() .with_child(if let Some(disclosed) = self.disclosed { - ActionButton::new_dynamic(self.action) + Button::dynamic_action(self.action) .with_id(self.id) .with_contents(Svg::new(if disclosed { "icons/file_icons/chevron_down.svg" @@ -131,7 +122,7 @@ pub mod disclosure { .with_child( self.content .with_style(self.style.content) - .render(v, cx) + .render(cx) .flex(1., true), ) .align_children_center() @@ -141,7 +132,7 @@ pub mod disclosure { } pub mod toggle { - use gpui::elements::{Component, StyleableComponent}; + use gpui::elements::{Component, SafeStylable}; use crate::Toggleable; @@ -151,7 +142,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -161,7 +152,7 @@ pub mod toggle { } } - impl StyleableComponent for Toggle { + impl SafeStylable for Toggle { type Style = Toggleable; type Output = Toggle; @@ -175,15 +166,11 @@ pub mod toggle { } } - impl Component for Toggle> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for Toggle> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { self.component .with_style(self.style.in_state(self.active).clone()) - .render(v, cx) + .render(cx) } } } @@ -192,9 +179,7 @@ pub mod action_button { use std::borrow::Cow; use gpui::{ - elements::{ - Component, ContainerStyle, MouseEventHandler, StyleableComponent, TooltipStyle, - }, + elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, }; @@ -216,7 +201,7 @@ pub mod action_button { contents: C, } - pub struct ActionButton { + pub struct Button { action: Box, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, @@ -225,8 +210,8 @@ pub mod action_button { style: Interactive, } - impl ActionButton<(), ()> { - pub fn new_dynamic(action: Box) -> Self { + impl Button<(), ()> { + pub fn dynamic_action(action: Box) -> Self { Self { contents: (), tag: action.type_tag(), @@ -237,8 +222,8 @@ pub mod action_button { } } - pub fn new(action: A) -> Self { - Self::new_dynamic(Box::new(action)) + pub fn action(action: A) -> Self { + Self::dynamic_action(Box::new(action)) } pub fn with_tooltip( @@ -255,8 +240,8 @@ pub mod action_button { self } - pub fn with_contents(self, contents: C) -> ActionButton { - ActionButton { + pub fn with_contents(self, contents: C) -> Button { + Button { action: self.action, tag: self.tag, style: self.style, @@ -267,12 +252,12 @@ pub mod action_button { } } - impl StyleableComponent for ActionButton { + impl SafeStylable for Button { type Style = Interactive>; - type Output = ActionButton>; + type Output = Button>; fn with_style(self, style: Self::Style) -> Self::Output { - ActionButton { + Button { action: self.action, tag: self.tag, contents: self.contents, @@ -283,14 +268,14 @@ pub mod action_button { } } - impl Component for ActionButton> { - fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + impl Component for Button> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents .with_style(style.contents.to_owned()) - .render(v, cx) + .render(cx) .contained() .with_style(style.container) .constrained(); @@ -335,7 +320,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{Component, Empty, StyleableComponent}, + elements::{Component, Empty, SafeStylable}, Element, }; use schemars::JsonSchema; @@ -417,7 +402,7 @@ pub mod svg { } } - impl StyleableComponent for Svg<()> { + impl SafeStylable for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -431,11 +416,7 @@ pub mod svg { } impl Component for Svg { - fn render( - self, - _: &mut V, - _: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { if let Some(path) = self.path { gpui::elements::Svg::new(path) .with_color(self.style.color) @@ -455,7 +436,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{Component, LabelStyle, StyleableComponent}, + elements::{Component, LabelStyle, SafeStylable}, Element, }; @@ -473,7 +454,7 @@ pub mod label { } } - impl StyleableComponent for Label<()> { + impl SafeStylable for Label<()> { type Style = LabelStyle; type Output = Label; @@ -487,11 +468,7 @@ pub mod label { } impl Component for Label { - fn render( - self, - _: &mut V, - _: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { gpui::elements::Label::new(self.text, self.style).into_any() } } From bfd3e53dcd0912f0534a5fff9c18beafba42cca0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 16:29:24 -0700 Subject: [PATCH 005/142] Implement component test page --- Cargo.lock | 3 + crates/component_test/Cargo.toml | 18 +++ crates/component_test/src/component_test.rs | 122 ++++++++++++++++++++ crates/gpui/src/elements/component.rs | 8 +- crates/gpui/src/elements/flex.rs | 33 +++++- crates/theme/src/components.rs | 18 +-- crates/theme/src/theme.rs | 10 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + styles/src/style_tree/app.ts | 5 +- styles/src/style_tree/component_test.ts | 19 +++ 11 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 crates/component_test/Cargo.toml create mode 100644 crates/component_test/src/component_test.rs create mode 100644 styles/src/style_tree/component_test.ts diff --git a/Cargo.lock b/Cargo.lock index 01c74fb4ceb62f59d322df7ba8355c3c1414de52..0081626cabe162ff9faad9a779ad27bbe9390005 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,7 +1566,9 @@ dependencies = [ name = "component_test" version = "0.1.0" dependencies = [ + "anyhow", "gpui", + "project", "settings", "theme", "util", @@ -9668,6 +9670,7 @@ dependencies = [ "collab_ui", "collections", "command_palette", + "component_test", "context_menu", "copilot", "copilot_button", diff --git a/crates/component_test/Cargo.toml b/crates/component_test/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..d714f6f72f62bf0c869439d07e937592a6ea644b --- /dev/null +++ b/crates/component_test/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "component_test" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/component_test.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +gpui = { path = "../gpui" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } +project = { path = "../project" } diff --git a/crates/component_test/src/component_test.rs b/crates/component_test/src/component_test.rs new file mode 100644 index 0000000000000000000000000000000000000000..30fd431290d3135ea5783e8a145be02f49b6933a --- /dev/null +++ b/crates/component_test/src/component_test.rs @@ -0,0 +1,122 @@ +use gpui::{ + actions, + color::Color, + elements::{Component, Flex, ParentElement, SafeStylable}, + AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use project::Project; +use theme::components::{action_button::Button, label::Label, ComponentExt}; +use workspace::{ + item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId, +}; + +pub fn init(cx: &mut AppContext) { + cx.add_action(ComponentTest::toggle_disclosure); + cx.add_action(ComponentTest::toggle_toggle); + cx.add_action(ComponentTest::deploy); + register_deserializable_item::(cx); +} + +actions!( + test, + [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest] +); + +struct ComponentTest { + disclosed: bool, + toggled: bool, +} + +impl ComponentTest { + fn new() -> Self { + Self { + disclosed: false, + toggled: false, + } + } + + fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext) { + workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx); + } + + fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext) { + self.disclosed = !self.disclosed; + cx.notify(); + } + + fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext) { + self.toggled = !self.toggled; + cx.notify(); + } +} + +impl Entity for ComponentTest { + type Event = (); +} + +impl View for ComponentTest { + fn ui_name() -> &'static str { + "Component Test" + } + + fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + let theme = theme::current(cx); + + PaneBackdrop::new( + cx.view_id(), + Flex::column() + .with_spacing(10.) + .with_child( + Button::action(NoAction) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Click me!")) + .with_style(theme.component_test.button.clone()) + .element(), + ) + .with_child( + Button::action(ToggleToggle) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Toggle me!")) + .toggleable(self.toggled) + .with_style(theme.component_test.toggle.clone()) + .element(), + ) + .with_child( + Label::new("A disclosure") + .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure)) + .with_style(theme.component_test.disclosure.clone()) + .element(), + ) + .constrained() + .with_width(200.) + .aligned() + .into_any(), + ) + .into_any() + } +} + +impl Item for ComponentTest { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + gpui::elements::Label::new("Component test", style.label.clone()).into_any() + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("ComponentTest") + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + _workspace_id: WorkspaceId, + _item_id: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + Task::ready(Ok(cx.add_view(|_| Self::new()))) + } +} diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index e391ff9b057edb6e8740d2ec7963bcc45421d67a..3776abcaa0ac75d725c8c98ed478e5b4a4630a01 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -128,7 +128,7 @@ impl StatefulComponent for C { pub trait StatefulStylable: StatefulComponent { type Style: Clone; - fn stateful_with_style(self, style: Self::Style) -> Self; + fn with_style(self, style: Self::Style) -> Self; } /// Same as SafeStylable, but generic over a view type @@ -136,7 +136,7 @@ pub trait StatefulSafeStylable { type Style: Clone; type Output: StatefulComponent; - fn stateful_with_style(self, style: Self::Style) -> Self::Output; + fn with_style(self, style: Self::Style) -> Self::Output; } /// Converting from stateless to stateful @@ -145,7 +145,7 @@ impl StatefulSafeStylable for C { type Output = C::Output; - fn stateful_with_style(self, style: Self::Style) -> Self::Output { + fn with_style(self, style: Self::Style) -> Self::Output { self.with_style(style) } } @@ -192,7 +192,7 @@ impl, V: View> StatefulSafeStylable for StatefulStyla type Output = C; - fn stateful_with_style(self, _: Self::Style) -> Self::Output { + fn with_style(self, _: Self::Style) -> Self::Output { self.component } } diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 3000b9575d5824ea42fd09cb5d10de4353434b25..9d175afc03ae3183772a1299bbfdabf22362f6da 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -22,6 +22,7 @@ pub struct Flex { children: Vec>, scroll_state: Option<(ElementStateHandle>, usize)>, child_alignment: f32, + spacing: f32, } impl Flex { @@ -31,6 +32,7 @@ impl Flex { children: Default::default(), scroll_state: None, child_alignment: -1., + spacing: 0., } } @@ -51,6 +53,11 @@ impl Flex { self } + pub fn with_spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + pub fn scrollable( mut self, element_id: usize, @@ -81,7 +88,8 @@ impl Flex { cx: &mut LayoutContext, ) { let cross_axis = self.axis.invert(); - for child in &mut self.children { + let last = self.children.len() - 1; + for (ix, child) in &mut self.children.iter_mut().enumerate() { if let Some(metadata) = child.metadata::() { if let Some((flex, expanded)) = metadata.flex { if expanded != layout_expanded { @@ -93,6 +101,10 @@ impl Flex { } else { let space_per_flex = *remaining_space / *remaining_flex; space_per_flex * flex + } - if ix == 0 || ix == last { + self.spacing / 2. + } else { + self.spacing }; let child_min = if expanded { child_max } else { 0. }; let child_constraint = match self.axis { @@ -137,7 +149,8 @@ impl Element for Flex { let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; - for child in &mut self.children { + let last = self.children.len().saturating_sub(1); + for (ix, child) in &mut self.children.iter_mut().enumerate() { let metadata = child.metadata::(); contains_float |= metadata.map_or(false, |metadata| metadata.float); @@ -155,7 +168,12 @@ impl Element for Flex { ), }; let size = child.layout(child_constraint, view, cx); - fixed_space += size.along(self.axis); + fixed_space += size.along(self.axis) + + if ix == 0 || ix == last { + self.spacing / 2. + } else { + self.spacing + }; cross_axis_max = cross_axis_max.max(size.along(cross_axis)); } } @@ -315,7 +333,8 @@ impl Element for Flex { } } - for child in &mut self.children { + let last = self.children.len().saturating_sub(1); + for (ix, child) in &mut self.children.iter_mut().enumerate() { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -353,9 +372,11 @@ impl Element for Flex { child.paint(scene, aligned_child_origin, visible_bounds, view, cx); + let spacing = if ix == last { 0. } else { self.spacing }; + match self.axis { - Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), - Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), + Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing), } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index d9a857915c53f0f70dcb8849abbc3fea538551af..fc208954cf6ed7773b2f80e1e4cde540343e8d8a 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -74,8 +74,9 @@ pub mod disclosure { } impl Disclosable { - pub fn with_id(self, id: usize) -> Disclosable { - Disclosable { id, ..self } + pub fn with_id(mut self, id: usize) -> Disclosable { + self.id = id; + self } } @@ -181,7 +182,7 @@ pub mod action_button { use gpui::{ elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, - Action, Element, TypeTag, View, + Action, Element, EventContext, TypeTag, View, }; use schemars::JsonSchema; use serde_derive::Deserialize; @@ -211,14 +212,14 @@ pub mod action_button { } impl Button<(), ()> { - pub fn dynamic_action(action: Box) -> Self { + pub fn dynamic_action(action: Box) -> Button<(), ()> { Self { contents: (), tag: action.type_tag(), + action, style: Interactive::new_blank(), tooltip: None, id: 0, - action, } } @@ -292,7 +293,7 @@ pub mod action_button { }) .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); - move |_, _, cx| { + move |_, _, cx: &mut EventContext| { let window = cx.window(); let view = cx.view_id(); let action = action.boxed_clone(); @@ -437,6 +438,7 @@ pub mod label { use gpui::{ elements::{Component, LabelStyle, SafeStylable}, + fonts::TextStyle, Element, }; @@ -455,14 +457,14 @@ pub mod label { } impl SafeStylable for Label<()> { - type Style = LabelStyle; + type Style = TextStyle; type Output = Label; fn with_style(self, style: Self::Style) -> Self::Output { Label { text: self.text, - style, + style: style.into(), } } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 08da9af29912a794cf6893911416b0e2525e8d8b..0f34963708deae3797b88a2217f29860b2eeb74a 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,7 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::{disclosure::DisclosureStyle, ToggleIconButtonStyle}; +use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle}; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -66,6 +66,7 @@ pub struct Theme { pub feedback: FeedbackStyle, pub welcome: WelcomeStyle, pub titlebar: Titlebar, + pub component_test: ComponentTest, } #[derive(Deserialize, Default, Clone, JsonSchema)] @@ -260,6 +261,13 @@ pub struct CollabPanel { pub face_overlap: f32, } +#[derive(Deserialize, Default, JsonSchema)] +pub struct ComponentTest { + pub button: Interactive>, + pub toggle: Toggleable>>, + pub disclosure: DisclosureStyle, +} + #[derive(Deserialize, Default, JsonSchema)] pub struct TabbedModal { pub tab_button: Toggleable>, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 988648d4b155d182889273eb524b83b0e2d13d59..cb66a6b587332b20075da80a63be93bf293890ba 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -25,6 +25,7 @@ cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } +component_test = { path = "../component_test" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index deef3b85eba1cadc377be2e9d8dd9249cb5aa636..caeaecededf585df32750555ab5a1bdd6a69e380 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -166,6 +166,7 @@ fn main() { terminal_view::init(cx); copilot::init(http.clone(), node_runtime, cx); ai::init(cx); + component_test::init(cx); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index ee5e19e11137a48097c6873dfca03d00c68d2279..cf7e0538a72874674700410b9d892fd057bfa0ca 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -12,7 +12,6 @@ import simple_message_notification from "./simple_message_notification" import project_shared_notification from "./project_shared_notification" import tooltip from "./tooltip" import terminal from "./terminal" -import contact_finder from "./contact_finder" import collab_panel from "./collab_panel" import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import incoming_call_notification from "./incoming_call_notification" @@ -22,6 +21,7 @@ import assistant from "./assistant" import { titlebar } from "./titlebar" import editor from "./editor" import feedback from "./feedback" +import component_test from "./component_test" import { useTheme } from "../common" export default function app(): any { @@ -54,6 +54,7 @@ export default function app(): any { tooltip: tooltip(), terminal: terminal(), assistant: assistant(), - feedback: feedback() + feedback: feedback(), + component_test: component_test(), } } diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts new file mode 100644 index 0000000000000000000000000000000000000000..eadbb5c2f1750e34b4e3ba99adecd8fff575abda --- /dev/null +++ b/styles/src/style_tree/component_test.ts @@ -0,0 +1,19 @@ +import { toggle_label_button_style } from "../component/label_button" +import { useTheme } from "../common" +import { text_button } from "../component/text_button" +import { toggleable_icon_button } from "../component/icon_button" +import { text } from "./components" + +export default function contacts_panel(): any { + const theme = useTheme() + + return { + button: text_button({}), + toggle: toggle_label_button_style({ active_color: "accent" }), + disclosure: { + ...text(theme.lowest, "sans", "base"), + button: toggleable_icon_button(theme, {}), + spacing: 4, + } + } +} From 3d89cd10a4c544bb4973861023f1da53954ecbf0 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 21 Aug 2023 16:35:57 +0200 Subject: [PATCH 006/142] added sha1 encoding for each document --- Cargo.lock | 411 ++++++++++---------- crates/semantic_index/Cargo.toml | 1 + crates/semantic_index/src/db.rs | 21 +- crates/semantic_index/src/embedding.rs | 2 +- crates/semantic_index/src/parsing.rs | 14 + crates/semantic_index/src/semantic_index.rs | 3 +- 6 files changed, 245 insertions(+), 207 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3edf9acab3911af43eb3e84055cb79892e26a27c..8048398ef82aba67c7a37b57d783d2d02d036dd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -140,7 +140,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3 dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -151,7 +151,7 @@ dependencies = [ "alacritty_config", "alacritty_config_derive", "base64 0.13.1", - "bitflags 2.3.3", + "bitflags 2.4.0", "home", "libc", "log", @@ -268,9 +268,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", @@ -278,9 +278,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayref" @@ -337,7 +337,7 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -351,7 +351,7 @@ dependencies = [ "futures-core", "futures-io", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -411,15 +411,15 @@ dependencies = [ "polling", "rustix 0.37.23", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", ] @@ -482,7 +482,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -505,7 +505,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "wasm-bindgen-futures", @@ -519,7 +519,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -530,7 +530,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -567,13 +567,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -586,7 +586,7 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tungstenite 0.16.0", ] @@ -681,12 +681,12 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.9", + "itoa", "matchit", "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -727,7 +727,7 @@ dependencies = [ "futures-util", "http", "mime", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "tokio", @@ -831,7 +831,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.28", + "syn 2.0.29", "which", ] @@ -858,9 +858,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -996,7 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "serde", ] @@ -1156,11 +1156,12 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -1251,9 +1252,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.19" +version = "4.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +checksum = "03aef18ddf7d879c15ce20f04826ef8418101c7e528014c3eeea13321047dca3" dependencies = [ "clap_builder", "clap_derive 4.3.12", @@ -1262,9 +1263,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.19" +version = "4.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +checksum = "f8ce6fffb678c9b80a70b6b6de0aad31df727623a70fd9a842c30cd573e2fa98" dependencies = [ "anstream", "anstyle", @@ -1294,7 +1295,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1355,7 +1356,7 @@ dependencies = [ "sum_tree", "tempfile", "thiserror", - "time 0.3.24", + "time 0.3.25", "tiny_http", "url", "util", @@ -1457,7 +1458,7 @@ dependencies = [ "sha-1 0.9.8", "sqlx", "theme", - "time 0.3.24", + "time 0.3.25", "tokio", "tokio-tungstenite", "toml 0.5.11", @@ -1984,7 +1985,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi 0.3.9", ] @@ -2065,9 +2066,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" dependencies = [ "serde", ] @@ -2246,9 +2247,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "editor" @@ -2361,9 +2362,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070" +checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a" dependencies = [ "serde", ] @@ -2526,13 +2527,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "windows-sys", ] @@ -2544,9 +2545,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide 0.7.1", @@ -2687,7 +2688,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", - "time 0.3.24", + "time 0.3.25", "util", ] @@ -2825,7 +2826,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "waker-fn", ] @@ -2837,7 +2838,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -2866,7 +2867,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "tokio-io", @@ -2989,11 +2990,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "bstr", "fnv", "log", @@ -3078,7 +3079,7 @@ dependencies = [ "smol", "sqlez", "sum_tree", - "time 0.3.24", + "time 0.3.25", "tiny-skia", "usvg", "util", @@ -3293,7 +3294,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes 1.4.0", "fnv", - "itoa 1.0.9", + "itoa", ] [[package]] @@ -3304,7 +3305,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.4.0", "http", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -3321,9 +3322,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human_bytes" @@ -3352,9 +3353,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.9", - "pin-project-lite 0.2.10", - "socket2", + "itoa", + "pin-project-lite 0.2.12", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -3368,7 +3369,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tokio-io-timeout", ] @@ -3586,7 +3587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -3626,12 +3627,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.9" @@ -4058,9 +4053,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ "serde", "value-bag", @@ -4091,9 +4086,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.94.0" +version = "0.94.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" dependencies = [ "bitflags 1.3.2", "serde", @@ -4751,9 +4746,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", @@ -4772,7 +4767,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4783,9 +4778,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -4920,7 +4915,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -5012,12 +5007,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.0", ] [[package]] @@ -5045,22 +5040,22 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5071,9 +5066,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -5098,7 +5093,7 @@ dependencies = [ "line-wrap", "quick-xml", "serde", - "time 0.3.24", + "time 0.3.25", ] [[package]] @@ -5163,7 +5158,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "windows-sys", ] @@ -5213,7 +5208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5553,9 +5548,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -5778,13 +5773,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -5799,11 +5794,11 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", "regex-syntax 0.7.4", ] @@ -5873,7 +5868,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -6093,7 +6088,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.28", + "syn 2.0.29", "walkdir", ] @@ -6164,7 +6159,7 @@ dependencies = [ "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes 0.5.3", - "itoa 1.0.9", + "itoa", "libc", "linux-raw-sys 0.0.42", "once_cell", @@ -6187,11 +6182,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno 0.3.2", "libc", "linux-raw-sys 0.4.5", @@ -6393,7 +6388,7 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.24", + "time 0.3.25", "tracing", "url", "uuid 1.4.1", @@ -6421,7 +6416,7 @@ dependencies = [ "rust_decimal", "sea-query-derive", "serde_json", - "time 0.3.24", + "time 0.3.25", "uuid 1.4.1", ] @@ -6436,7 +6431,7 @@ dependencies = [ "sea-query", "serde_json", "sqlx", - "time 0.3.24", + "time 0.3.25", "uuid 1.4.1", ] @@ -6564,10 +6559,11 @@ dependencies = [ "serde", "serde_json", "settings", + "sha1", "smol", "tempdir", "theme", - "tiktoken-rs 0.5.0", + "tiktoken-rs 0.5.1", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -6615,22 +6611,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6655,24 +6651,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "indexmap 2.0.0", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] [[package]] name = "serde_json_lenient" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" +checksum = "29591aaa3a13f5ad0f2dd1a8a21bcddab11eaae7c3522b20ade2e85e9df52206" dependencies = [ - "indexmap 1.9.3", - "itoa 0.4.8", + "indexmap 2.0.0", + "itoa", "ryu", "serde", ] @@ -6685,7 +6681,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6704,7 +6700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] @@ -6991,6 +6987,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -7090,7 +7096,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "indexmap 1.9.3", - "itoa 1.0.9", + "itoa", "libc", "libsqlite3-sys", "log", @@ -7113,7 +7119,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", - "time 0.3.24", + "time 0.3.25", "tokio-stream", "url", "uuid 1.4.1", @@ -7236,7 +7242,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7247,7 +7253,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7312,9 +7318,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -7398,14 +7404,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -7552,22 +7558,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7614,9 +7620,9 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a99d843674a3468b4a9200a565bbe909a0152f95e82a52feae71e6bf2d4b49d" +checksum = "2bf14cb08d8fda6e484c75ec2bfb6bcef48347d47abcd011fa9d56ee995a3da0" dependencies = [ "anyhow", "base64 0.21.2", @@ -7640,12 +7646,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" dependencies = [ "deranged", - "itoa 1.0.9", + "itoa", "serde", "time-core", "time-macros", @@ -7710,20 +7716,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes 1.4.0", "libc", "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -7745,7 +7750,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7757,7 +7762,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7788,7 +7793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7814,7 +7819,7 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7828,7 +7833,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tracing", ] @@ -7917,7 +7922,7 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "rand 0.8.5", "slab", "tokio", @@ -7940,7 +7945,7 @@ dependencies = [ "http", "http-body", "http-range-header", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tower", "tower-layer", "tower-service", @@ -7966,7 +7971,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tracing-attributes", "tracing-core", ] @@ -7979,7 +7984,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -8064,9 +8069,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.20.4" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1bb73a4101c88775e4fefcd0543ee25e192034484a5bd45cb99eefb997dca9" +checksum = "30b03bdf218020057abee831581a74bff8c298323d6c6cd1a70556430ded9f4b" dependencies = [ "cc", "tree-sitter", @@ -8213,9 +8218,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47ebd9cac632764b2f4389b08517bf2ef895431dd163eb562e3d2062cc23a14" +checksum = "e6c93b1b1fbd0d399db3445f51fd3058e43d0b4dcff62ddbdb46e66550978aa5" dependencies = [ "cc", "tree-sitter", @@ -8242,9 +8247,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797842733e252dc11ae5d403a18060bf337b822fc2ae5ddfaa6ff4d9cc20bda6" +checksum = "b0832309b0b2b6d33760ce5c0e818cb47e1d72b468516bfe4134408926fa7594" dependencies = [ "cc", "tree-sitter", @@ -8773,7 +8778,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -8807,7 +8812,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9044,9 +9049,9 @@ dependencies = [ [[package]] name = "wast" -version = "62.0.1" +version = "63.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ae06f09dbe377b889fbd620ff8fa21e1d49d1d9d364983c0cdbf9870cb9f1f" +checksum = "2560471f60a48b77fccefaf40796fda61c97ce1e790b59dfcec9dc3995c9f63a" dependencies = [ "leb128", "memchr", @@ -9056,11 +9061,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842e15861d203fb4a96d314b0751cdeaf0f6f8b35e8d81d2953af2af5e44e637" +checksum = "3bdc306c2c4c2f2bf2ba69e083731d0d2a77437fc6a350a19db139636e7e416c" dependencies = [ - "wast 62.0.1", + "wast 63.0.0", ] [[package]] @@ -9262,7 +9267,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9271,7 +9276,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9291,17 +9296,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -9312,9 +9317,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -9324,9 +9329,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -9336,9 +9341,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -9348,9 +9353,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -9360,9 +9365,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -9372,9 +9377,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -9384,15 +9389,15 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.2" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7" +checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] @@ -9522,7 +9527,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.3.19", + "clap 4.3.23", "schemars", "serde_json", "theme", @@ -9703,7 +9708,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 3c7a6ff5df6df4b0ae7de9e0e7754a0e0d5850cc..4e817fcbe2c6dc8a6edac00fc51dd9e5be437b8f 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -38,6 +38,7 @@ parking_lot.workspace = true rand.workspace = true schemars.workspace = true globset.workspace = true +sha1 = "0.10.5" [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index e57a5d733fb2a0b2c68dc6e874b8ac050de5e78b..60ecf3b45fef383e73172c11b0b5ee3d7d48d93d 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -26,6 +26,9 @@ pub struct FileRecord { #[derive(Debug)] struct Embedding(pub Vec); +#[derive(Debug)] +struct Sha1(pub Vec); + impl FromSql for Embedding { fn column_result(value: ValueRef) -> FromSqlResult { let bytes = value.as_blob()?; @@ -37,6 +40,17 @@ impl FromSql for Embedding { } } +impl FromSql for Sha1 { + fn column_result(value: ValueRef) -> FromSqlResult { + let bytes = value.as_blob()?; + let sha1: Result, Box> = bincode::deserialize(bytes); + if sha1.is_err() { + return Err(rusqlite::types::FromSqlError::Other(sha1.unwrap_err())); + } + return Ok(Sha1(sha1.unwrap())); + } +} + pub struct VectorDatabase { db: rusqlite::Connection, } @@ -132,6 +146,7 @@ impl VectorDatabase { end_byte INTEGER NOT NULL, name VARCHAR NOT NULL, embedding BLOB NOT NULL, + sha1 BLOB NOT NULL, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE )", [], @@ -182,15 +197,17 @@ impl VectorDatabase { // I imagine we can speed this up with a bulk insert of some kind. for document in documents { let embedding_blob = bincode::serialize(&document.embedding)?; + let sha_blob = bincode::serialize(&document.sha1)?; self.db.execute( - "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding) VALUES (?1, ?2, ?3, ?4, $5)", + "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding, sha1) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ file_id, document.range.start.to_string(), document.range.end.to_string(), document.name, - embedding_blob + embedding_blob, + sha_blob ], )?; } diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 77457ec7f6e34961ab2a784ef6f0d8068c4c1dbb..4fc247bfcc3233cc55e146fa356a64bcb837a09e 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -39,7 +39,7 @@ struct OpenAIEmbeddingResponse { #[derive(Debug, Deserialize)] struct OpenAIEmbedding { - embedding: Vec, + embedding: Vec, index: usize, object: String, } diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index cef23862c563f470000306fde5ac32f95a50a458..4aefb0b00d86df9ce6b0a927647687c20e050c83 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Ok, Result}; use language::{Grammar, Language}; +use sha1::{Digest, Sha1}; use std::{ cmp::{self, Reverse}, collections::HashSet, @@ -15,6 +16,7 @@ pub struct Document { pub range: Range, pub content: String, pub embedding: Vec, + pub sha1: [u8; 20], } const CODE_CONTEXT_TEMPLATE: &str = @@ -63,11 +65,15 @@ impl CodeContextRetriever { .replace("", language_name.as_ref()) .replace("", &content); + let mut sha1 = Sha1::new(); + sha1.update(&document_span); + Ok(vec![Document { range: 0..content.len(), content: document_span, embedding: Vec::new(), name: language_name.to_string(), + sha1: sha1.finalize().into(), }]) } @@ -76,11 +82,15 @@ impl CodeContextRetriever { .replace("", relative_path.to_string_lossy().as_ref()) .replace("", &content); + let mut sha1 = Sha1::new(); + sha1.update(&document_span); + Ok(vec![Document { range: 0..content.len(), content: document_span, embedding: Vec::new(), name: "Markdown".to_string(), + sha1: sha1.finalize().into(), }]) } @@ -253,11 +263,15 @@ impl CodeContextRetriever { ); } + let mut sha1 = Sha1::new(); + sha1.update(&document_content); + documents.push(Document { name, content: document_content, range: item_range.clone(), embedding: vec![], + sha1: sha1.finalize().into(), }) } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 5aaecac733a3d171d36704bda7f0051b6f4db79b..f567ca8770a7e54b695ba0370831c709b0501a4b 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -34,7 +34,7 @@ use util::{ ResultExt, }; -const SEMANTIC_INDEX_VERSION: usize = 6; +const SEMANTIC_INDEX_VERSION: usize = 7; const EMBEDDINGS_BATCH_SIZE: usize = 80; pub fn init( @@ -92,6 +92,7 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, + file_mtimes: HashMap, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, } From ced2b2aec3e9fee2e598e0a0125f843e05c4e906 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 22 Aug 2023 11:58:48 +0200 Subject: [PATCH 007/142] reworked ProjectState to include additional context --- crates/search/src/project_search.rs | 12 +- crates/semantic_index/src/embedding.rs | 2 +- crates/semantic_index/src/semantic_index.rs | 138 ++++++++++++++++-- .../src/semantic_index_tests.rs | 7 + 4 files changed, 146 insertions(+), 13 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 196d5589f4df881ddd60293d757d9640734f46a2..7e3585656a8925374365cebeb2265df0714f839c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -640,6 +640,7 @@ impl ProjectSearchView { self.search_options = SearchOptions::none(); let project = self.model.read(cx).project.clone(); + let index_task = semantic_index.update(cx, |semantic_index, cx| { semantic_index.index_project(project, cx) }); @@ -759,7 +760,7 @@ impl ProjectSearchView { } fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { - let project; + let mut project; let excerpts; let mut query_text = String::new(); let mut options = SearchOptions::NONE; @@ -843,6 +844,15 @@ impl ProjectSearchView { .detach(); let filters_enabled = false; + // Initialize Semantic Index if Needed + if SemanticIndex::enabled(cx) { + let model = model.read(cx); + project = model.project.clone(); + SemanticIndex::global(cx).map(|semantic| { + semantic.update(cx, |this, cx| this.initialize_project(project, cx)) + }); + } + // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { search_id: model.read(cx).search_id, diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 4fc247bfcc3233cc55e146fa356a64bcb837a09e..77457ec7f6e34961ab2a784ef6f0d8068c4c1dbb 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -39,7 +39,7 @@ struct OpenAIEmbeddingResponse { #[derive(Debug, Deserialize)] struct OpenAIEmbedding { - embedding: Vec, + embedding: Vec, index: usize, object: String, } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index f567ca8770a7e54b695ba0370831c709b0501a4b..2b803e36acb766d9b82900b3227d496fd810f08d 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -92,7 +92,8 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, - file_mtimes: HashMap, + worktree_file_mtimes: HashMap>, + subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, } @@ -113,6 +114,25 @@ impl JobHandle { } } impl ProjectState { + fn new( + subscription: gpui::Subscription, + worktree_db_ids: Vec<(WorktreeId, i64)>, + worktree_file_mtimes: HashMap>, + outstanding_job_count_rx: watch::Receiver, + _outstanding_job_count_tx: Arc>>, + ) -> Self { + let (job_count_tx, job_count_rx) = watch::channel_with(0); + let job_count_tx = Arc::new(Mutex::new(job_count_tx)); + + Self { + worktree_db_ids, + worktree_file_mtimes, + outstanding_job_count_rx, + _outstanding_job_count_tx, + subscription, + } + } + fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option { self.worktree_db_ids .iter() @@ -577,6 +597,84 @@ impl SemanticIndex { }) } + pub fn initialize_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) { + let worktree_scans_complete = project + .read(cx) + .worktrees(cx) + .map(|worktree| { + let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete(); + async move { + scan_complete.await; + } + }) + .collect::>(); + + let worktree_db_ids = project + .read(cx) + .worktrees(cx) + .map(|worktree| { + self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf()) + }) + .collect::>(); + + let _subscription = cx.subscribe(&project, |this, project, event, cx| { + if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { + todo!(); + // this.project_entries_changed(project, changes, cx, worktree_id); + } + }); + + cx.spawn(|this, mut cx| async move { + futures::future::join_all(worktree_scans_complete).await; + + let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; + let worktrees = project.read_with(&cx, |project, cx| { + project + .worktrees(cx) + .map(|worktree| worktree.read(cx).snapshot()) + .collect::>() + }); + + let mut worktree_file_mtimes = HashMap::new(); + let mut db_ids_by_worktree_id = HashMap::new(); + + for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { + let db_id = db_id?; + db_ids_by_worktree_id.insert(worktree.id(), db_id); + worktree_file_mtimes.insert( + worktree.id(), + this.read_with(&cx, |this, _| this.get_file_mtimes(db_id)) + .await?, + ); + } + + let worktree_db_ids = db_ids_by_worktree_id + .iter() + .map(|(a, b)| (*a, *b)) + .collect(); + + let (job_count_tx, job_count_rx) = watch::channel_with(0); + let job_count_tx = Arc::new(Mutex::new(job_count_tx)); + this.update(&mut cx, |this, _| { + let project_state = ProjectState::new( + _subscription, + worktree_db_ids, + worktree_file_mtimes.clone(), + job_count_rx, + job_count_tx, + ); + this.projects.insert(project.downgrade(), project_state); + }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + pub fn index_project( &mut self, project: ModelHandle, @@ -605,6 +703,22 @@ impl SemanticIndex { let db_update_tx = self.db_update_tx.clone(); let parsing_files_tx = self.parsing_files_tx.clone(); + let state = self.projects.get(&project.downgrade()); + let state = if state.is_none() { + return Task::Ready(Some(Err(anyhow!("Project not yet initialized")))); + } else { + state.unwrap() + }; + + let state = state.clone().to_owned(); + + let _subscription = cx.subscribe(&project, |this, project, event, _cx| { + if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { + todo!(); + // this.project_entries_changed(project, changes, cx, worktree_id); + } + }); + cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; @@ -629,20 +743,22 @@ impl SemanticIndex { ); } + let worktree_db_ids = db_ids_by_worktree_id + .iter() + .map(|(a, b)| (*a, *b)) + .collect(); + let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); this.update(&mut cx, |this, _| { - this.projects.insert( - project.downgrade(), - ProjectState { - worktree_db_ids: db_ids_by_worktree_id - .iter() - .map(|(a, b)| (*a, *b)) - .collect(), - outstanding_job_count_rx: job_count_rx.clone(), - _outstanding_job_count_tx: job_count_tx.clone(), - }, + let project_state = ProjectState::new( + _subscription, + worktree_db_ids, + worktree_file_mtimes.clone(), + job_count_rx.clone(), + job_count_tx.clone(), ); + this.projects.insert(project.downgrade(), project_state); }); cx.background() diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 07ddce4d37c641e45b46599db73c6686e6421949..0ac5953f0bb2a5771cc9c962ee095208b7895c10 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -86,6 +86,13 @@ async fn test_semantic_index(cx: &mut TestAppContext) { .unwrap(); let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + + store + .update(cx, |store, cx| { + store.initialize_project(project.clone(), cx) + }) + .await; + let (file_count, outstanding_file_count) = store .update(cx, |store, cx| store.index_project(project.clone(), cx)) .await From aabdfa210f3afbaea6929f2fae319a93b7ab8c67 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 22 Aug 2023 14:45:27 +0200 Subject: [PATCH 008/142] working on initialization + index breakup --- crates/search/src/project_search.rs | 2 +- crates/semantic_index/src/semantic_index.rs | 308 +++++++++++++------- 2 files changed, 197 insertions(+), 113 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7e3585656a8925374365cebeb2265df0714f839c..ca317c0ded71ab50ce46a1ec5f1af4a04a60991f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -849,7 +849,7 @@ impl ProjectSearchView { let model = model.read(cx); project = model.project.clone(); SemanticIndex::global(cx).map(|semantic| { - semantic.update(cx, |this, cx| this.initialize_project(project, cx)) + semantic.update(cx, |this, cx| this.initialize_project(project.clone(), cx)); }); } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 2b803e36acb766d9b82900b3227d496fd810f08d..8849b643c5ef00a71c22ebb9d2da5b2658d8057e 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -96,6 +96,7 @@ struct ProjectState { subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, + queue: HashMap>, } #[derive(Clone)] @@ -130,9 +131,25 @@ impl ProjectState { outstanding_job_count_rx, _outstanding_job_count_tx, subscription, + queue: HashMap::new(), } } + fn add_to_queue(&mut self, worktree_id: WorktreeId, operation: IndexOperation) { + if let Some(worktree_queue) = self.queue.get_mut(&worktree_id) { + worktree_queue.push(operation); + } else { + self.queue.insert(worktree_id, vec![operation]); + } + } + + fn pop(&mut self) -> Option { + self.queue + .iter_mut() + .next() + .and_then(|(_, mut entry)| entry.pop()) + } + fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option { self.worktree_db_ids .iter() @@ -158,6 +175,7 @@ impl ProjectState { } } +#[derive(Clone)] pub struct PendingFile { worktree_db_id: i64, relative_path: PathBuf, @@ -167,6 +185,12 @@ pub struct PendingFile { job_handle: JobHandle, } +#[derive(Clone)] +enum IndexOperation { + IndexFile { file: PendingFile }, + DeleteFile { file: PendingFile }, +} + pub struct SearchResult { pub buffer: ModelHandle, pub range: Range, @@ -628,102 +652,12 @@ impl SemanticIndex { } }); - cx.spawn(|this, mut cx| async move { - futures::future::join_all(worktree_scans_complete).await; - - let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; - let worktrees = project.read_with(&cx, |project, cx| { - project - .worktrees(cx) - .map(|worktree| worktree.read(cx).snapshot()) - .collect::>() - }); - - let mut worktree_file_mtimes = HashMap::new(); - let mut db_ids_by_worktree_id = HashMap::new(); - - for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { - let db_id = db_id?; - db_ids_by_worktree_id.insert(worktree.id(), db_id); - worktree_file_mtimes.insert( - worktree.id(), - this.read_with(&cx, |this, _| this.get_file_mtimes(db_id)) - .await?, - ); - } - - let worktree_db_ids = db_ids_by_worktree_id - .iter() - .map(|(a, b)| (*a, *b)) - .collect(); - - let (job_count_tx, job_count_rx) = watch::channel_with(0); - let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - this.update(&mut cx, |this, _| { - let project_state = ProjectState::new( - _subscription, - worktree_db_ids, - worktree_file_mtimes.clone(), - job_count_rx, - job_count_tx, - ); - this.projects.insert(project.downgrade(), project_state); - }); - - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - pub fn index_project( - &mut self, - project: ModelHandle, - cx: &mut ModelContext, - ) -> Task)>> { - let t0 = Instant::now(); - let worktree_scans_complete = project - .read(cx) - .worktrees(cx) - .map(|worktree| { - let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete(); - async move { - scan_complete.await; - } - }) - .collect::>(); - let worktree_db_ids = project - .read(cx) - .worktrees(cx) - .map(|worktree| { - self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf()) - }) - .collect::>(); - let language_registry = self.language_registry.clone(); - let db_update_tx = self.db_update_tx.clone(); - let parsing_files_tx = self.parsing_files_tx.clone(); - - let state = self.projects.get(&project.downgrade()); - let state = if state.is_none() { - return Task::Ready(Some(Err(anyhow!("Project not yet initialized")))); - } else { - state.unwrap() - }; - - let state = state.clone().to_owned(); - - let _subscription = cx.subscribe(&project, |this, project, event, _cx| { - if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { - todo!(); - // this.project_entries_changed(project, changes, cx, worktree_id); - } - }); cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; - let worktrees = project.read_with(&cx, |project, cx| { project .worktrees(cx) @@ -733,6 +667,7 @@ impl SemanticIndex { let mut worktree_file_mtimes = HashMap::new(); let mut db_ids_by_worktree_id = HashMap::new(); + for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { let db_id = db_id?; db_ids_by_worktree_id.insert(worktree.id(), db_id); @@ -761,10 +696,12 @@ impl SemanticIndex { this.projects.insert(project.downgrade(), project_state); }); - cx.background() + let worktree_files = cx + .background() .spawn(async move { - let mut count = 0; + let mut worktree_files = HashMap::new(); for worktree in worktrees.into_iter() { + let mut candidate_files = Vec::new(); let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); @@ -773,6 +710,7 @@ impl SemanticIndex { .language_for_file(&absolute_path, None) .await { + // Test if file is valid parseable file if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) && &language.name().as_ref() != &"Markdown" && language @@ -789,40 +727,186 @@ impl SemanticIndex { .map_or(false, |existing_mtime| existing_mtime == file.mtime); if !already_stored { - count += 1; - let job_handle = JobHandle::new(&job_count_tx); - parsing_files_tx - .try_send(PendingFile { + candidate_files.push(IndexOperation::IndexFile { + file: PendingFile { worktree_db_id: db_ids_by_worktree_id[&worktree.id()], relative_path: path_buf, absolute_path, language, job_handle, modified_time: file.mtime, - }) - .unwrap(); + }, + }); } } } - for file in file_mtimes.keys() { - db_update_tx - .try_send(DbOperation::Delete { - worktree_id: db_ids_by_worktree_id[&worktree.id()], - path: file.to_owned(), - }) - .unwrap(); - } + + worktree_files.insert(worktree.id(), candidate_files); } - log::trace!( - "walking worktree took {:?} milliseconds", - t0.elapsed().as_millis() - ); - anyhow::Ok((count, job_count_rx)) + anyhow::Ok(worktree_files) }) - .await + .await?; + + this.update(&mut cx, |this, cx| { + if let Some(project_state) = this.projects.get_mut(&project.downgrade()) { + for (worktree_id, index_operations) in &worktree_files { + for op in index_operations { + project_state.add_to_queue(*worktree_id, op.clone()); + } + } + } + }); + + cx.background().spawn(async move { anyhow::Ok(()) }).await + }) + .detach_and_log_err(cx) + } + + pub fn index_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) -> Task)>> { + let state = self.projects.get_mut(&project.downgrade()); + let state = if state.is_none() { + return Task::Ready(Some(Err(anyhow!("Project not yet initialized")))); + } else { + state.unwrap() + }; + + let parsing_files_tx = self.parsing_files_tx.clone(); + let db_update_tx = self.db_update_tx.clone(); + let job_count_rx = state.outstanding_job_count_rx.clone(); + let count = state.queue.values().map(Vec::len).sum(); + cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |this, cx| { + let Some(mut state) = this.projects.get_mut(&project.downgrade()) else { + return; + }; + let Some(mut index_operation) = state.pop() else { return;}; + let _ = match index_operation { + IndexOperation::IndexFile { file } => { + parsing_files_tx.try_send(file); + } + IndexOperation::DeleteFile { file } => { + db_update_tx.try_send(DbOperation::Delete { + worktree_id: file.worktree_db_id, + path: file.relative_path, + }); + } + }; + }); }) + .detach(); + + Task::Ready(Some(Ok((count, job_count_rx)))) + + // cx.spawn(|this, mut cx| async move { + // futures::future::join_all(worktree_scans_complete).await; + + // let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; + + // let worktrees = project.read_with(&cx, |project, cx| { + // project + // .worktrees(cx) + // .map(|worktree| worktree.read(cx).snapshot()) + // .collect::>() + // }); + + // let mut worktree_file_mtimes = HashMap::new(); + // let mut db_ids_by_worktree_id = HashMap::new(); + // for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { + // let db_id = db_id?; + // db_ids_by_worktree_id.insert(worktree.id(), db_id); + // worktree_file_mtimes.insert( + // worktree.id(), + // this.read_with(&cx, |this, _| this.get_file_mtimes(db_id)) + // .await?, + // ); + // } + + // let worktree_db_ids = db_ids_by_worktree_id + // .iter() + // .map(|(a, b)| (*a, *b)) + // .collect(); + + // let (job_count_tx, job_count_rx) = watch::channel_with(0); + // let job_count_tx = Arc::new(Mutex::new(job_count_tx)); + // this.update(&mut cx, |this, _| { + // let project_state = ProjectState::new( + // _subscription, + // worktree_db_ids, + // worktree_file_mtimes.clone(), + // job_count_rx.clone(), + // job_count_tx.clone(), + // ); + // this.projects.insert(project.downgrade(), project_state); + // }); + + // cx.background() + // .spawn(async move { + // let mut count = 0; + // for worktree in worktrees.into_iter() { + // let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); + // for file in worktree.files(false, 0) { + // let absolute_path = worktree.absolutize(&file.path); + + // if let Ok(language) = language_registry + // .language_for_file(&absolute_path, None) + // .await + // { + // if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) + // && &language.name().as_ref() != &"Markdown" + // && language + // .grammar() + // .and_then(|grammar| grammar.embedding_config.as_ref()) + // .is_none() + // { + // continue; + // } + + // let path_buf = file.path.to_path_buf(); + // let stored_mtime = file_mtimes.remove(&file.path.to_path_buf()); + // let already_stored = stored_mtime + // .map_or(false, |existing_mtime| existing_mtime == file.mtime); + + // if !already_stored { + // count += 1; + + // let job_handle = JobHandle::new(&job_count_tx); + // parsing_files_tx + // .try_send(PendingFile { + // worktree_db_id: db_ids_by_worktree_id[&worktree.id()], + // relative_path: path_buf, + // absolute_path, + // language, + // job_handle, + // modified_time: file.mtime, + // }) + // .unwrap(); + // } + // } + // } + // for file in file_mtimes.keys() { + // db_update_tx + // .try_send(DbOperation::Delete { + // worktree_id: db_ids_by_worktree_id[&worktree.id()], + // path: file.to_owned(), + // }) + // .unwrap(); + // } + // } + + // log::trace!( + // "walking worktree took {:?} milliseconds", + // t0.elapsed().as_millis() + // ); + // anyhow::Ok((count, job_count_rx)) + // }) + // .await + // }) } pub fn outstanding_job_count_rx( From 328b7e523c4d7897380af9f3ec17c8f2e904356d Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 22 Aug 2023 15:01:21 +0200 Subject: [PATCH 009/142] reorganized to stop the race --- crates/semantic_index/src/semantic_index.rs | 34 ++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 8849b643c5ef00a71c22ebb9d2da5b2658d8057e..79e649838a6a7418da0e9c92346e71529a003620 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -685,17 +685,19 @@ impl SemanticIndex { let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - this.update(&mut cx, |this, _| { - let project_state = ProjectState::new( - _subscription, - worktree_db_ids, - worktree_file_mtimes.clone(), - job_count_rx.clone(), - job_count_tx.clone(), - ); - this.projects.insert(project.downgrade(), project_state); - }); - + let job_count_tx_longlived = job_count_tx.clone(); + // this.update(&mut cx, |this, _| { + // let project_state = ProjectState::new( + // _subscription, + // worktree_db_ids, + // worktree_file_mtimes.clone(), + // job_count_rx.clone(), + // job_count_tx.clone(), + // ); + // this.projects.insert(project.downgrade(), project_state); + // }); + + let worktree_file_mtimes_all = worktree_file_mtimes.clone(); let worktree_files = cx .background() .spawn(async move { @@ -750,6 +752,14 @@ impl SemanticIndex { .await?; this.update(&mut cx, |this, cx| { + let project_state = ProjectState::new( + _subscription, + worktree_db_ids, + worktree_file_mtimes_all, + job_count_rx, + job_count_tx_longlived, + ); + if let Some(project_state) = this.projects.get_mut(&project.downgrade()) { for (worktree_id, index_operations) in &worktree_files { for op in index_operations { @@ -757,6 +767,8 @@ impl SemanticIndex { } } } + + this.projects.insert(project.downgrade(), project_state); }); cx.background().spawn(async move { anyhow::Ok(()) }).await From 471810a3c2ce1ebf2b4ebdbaaa5e94e43e52ac5b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 22 Aug 2023 15:27:44 -0400 Subject: [PATCH 010/142] WIP Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- assets/settings/default.json | 18 ++++++++---- crates/project/src/terminals.rs | 49 +++++++++++++++++++++++++++++++++ crates/terminal/src/terminal.rs | 14 ++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 24412b883bf0be12cb2639dd54dec7f70adf6882..f4d77f02cf5425888b5b238a2284586fefd4d09b 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -284,8 +284,6 @@ // "directory": "~/zed/projects/" // } // } - // - // "working_directory": "current_project_directory", // Set the cursor blinking behavior in the terminal. // May take 4 values: @@ -334,13 +332,23 @@ // "line_height": { // "custom": 2 // }, - "line_height": "comfortable" + "line_height": "comfortable", // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. - // "font_size": "15" + // "font_size": "15", // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. - // "font_family": "Zed Mono" + // "font_family": "Zed Mono", + // --- + // Whether or not to automatically search for, and activate, Python virtual + // environments. + // Current limitations: + // - Only ".env", "env", ".venv", and "venv" are searched for at the + // root of the project + // - Only works with Posix-complaint shells + // - Only activates the first virtual environment it finds, regardless + // of the nunber of projects in the workspace. + "automatically_activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index db5996829fa278db04e793d751d02ace086594e3..e585b659ee3ca0533c801ba7b7c2840402e82a08 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -3,6 +3,9 @@ use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; use std::path::PathBuf; use terminal::{Terminal, TerminalBuilder, TerminalSettings}; +#[cfg(target_os = "macos")] +use std::os::unix::ffi::OsStrExt; + pub struct Terminals { pub(crate) local_handles: Vec>, } @@ -47,6 +50,12 @@ impl Project { }) .detach(); + let setting = settings::get::(cx); + + if setting.automatically_activate_python_virtual_environment { + self.set_up_python_virtual_environment(&terminal_handle, cx); + } + terminal_handle }); @@ -54,6 +63,46 @@ impl Project { } } + fn set_up_python_virtual_environment( + &mut self, + terminal_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + let virtual_environment = self.find_python_virtual_environment(cx); + if let Some(virtual_environment) = virtual_environment { + // Paths are not strings so we need to jump through some hoops to format the command without `format!` + let mut command = Vec::from("source ".as_bytes()); + command.extend_from_slice(virtual_environment.as_os_str().as_bytes()); + command.push(b'\n'); + + terminal_handle.update(cx, |this, _| this.input_bytes(command)); + } + } + + pub fn find_python_virtual_environment( + &mut self, + cx: &mut ModelContext, + ) -> Option { + const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + + let worktree_paths = self + .worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()); + + for worktree_path in worktree_paths { + for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { + let mut path = worktree_path.join(virtual_environment_name); + path.push("bin/activate"); + + if path.exists() { + return Some(path); + } + } + } + + None + } + pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3bae06a86dc754126effb1c3c3302a31315d246c..9b0f0bbc860b113de7c2e933bfdab76b25c27e02 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -158,6 +158,7 @@ pub struct TerminalSettings { pub dock: TerminalDockPosition, pub default_width: f32, pub default_height: f32, + pub automatically_activate_python_virtual_environment: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -176,6 +177,7 @@ pub struct TerminalSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, + pub automatically_activate_python_virtual_environment: Option, } impl TerminalSettings { @@ -1018,6 +1020,10 @@ impl Terminal { self.pty_tx.notify(input.into_bytes()); } + fn write_bytes_to_pty(&self, input: Vec) { + self.pty_tx.notify(input); + } + pub fn input(&mut self, input: String) { self.events .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); @@ -1026,6 +1032,14 @@ impl Terminal { self.write_to_pty(input); } + pub fn input_bytes(&mut self, input: Vec) { + self.events + .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); + self.events.push_back(InternalEvent::SetSelection(None)); + + self.write_bytes_to_pty(input); + } + pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool { let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta); if let Some(esc) = esc { From 711f156308322edeeb195978e5773e020ecdf918 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 04:04:36 -0400 Subject: [PATCH 011/142] WIP --- crates/project/src/terminals.rs | 72 +++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index e585b659ee3ca0533c801ba7b7c2840402e82a08..0fa525a82ce0a754d572d9f8ccf82c9530c9fcb2 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,7 @@ use crate::Project; use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; use std::path::PathBuf; -use terminal::{Terminal, TerminalBuilder, TerminalSettings}; +use terminal::{Shell, Terminal, TerminalBuilder, TerminalSettings}; #[cfg(target_os = "macos")] use std::os::unix::ffi::OsStrExt; @@ -23,10 +23,14 @@ impl Project { )); } else { let settings = settings::get::(cx); + let automatically_activate_python_virtual_environment = settings + .automatically_activate_python_virtual_environment + .clone(); + let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( working_directory.clone(), - settings.shell.clone(), + shell.clone(), settings.env.clone(), Some(settings.blinking.clone()), settings.alternate_scroll, @@ -50,10 +54,13 @@ impl Project { }) .detach(); - let setting = settings::get::(cx); - - if setting.automatically_activate_python_virtual_environment { - self.set_up_python_virtual_environment(&terminal_handle, cx); + if automatically_activate_python_virtual_environment { + let activate_script_path = self.find_activate_script_path(&shell, cx); + self.activate_python_virtual_environment( + activate_script_path, + &terminal_handle, + cx, + ); } terminal_handle @@ -63,36 +70,35 @@ impl Project { } } - fn set_up_python_virtual_environment( - &mut self, - terminal_handle: &ModelHandle, - cx: &mut ModelContext, - ) { - let virtual_environment = self.find_python_virtual_environment(cx); - if let Some(virtual_environment) = virtual_environment { - // Paths are not strings so we need to jump through some hoops to format the command without `format!` - let mut command = Vec::from("source ".as_bytes()); - command.extend_from_slice(virtual_environment.as_os_str().as_bytes()); - command.push(b'\n'); - - terminal_handle.update(cx, |this, _| this.input_bytes(command)); - } - } - - pub fn find_python_virtual_environment( + pub fn find_activate_script_path( &mut self, + shell: &Shell, cx: &mut ModelContext, ) -> Option { - const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + let program = match shell { + terminal::Shell::System => "Figure this out", + terminal::Shell::Program(program) => program, + terminal::Shell::WithArguments { program, args } => program, + }; + + // This is so hacky - find a better way to do this + let script_name = if program.contains("fish") { + "activate.fish" + } else { + "activate" + }; let worktree_paths = self .worktrees(cx) .map(|worktree| worktree.read(cx).abs_path()); + const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + for worktree_path in worktree_paths { for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { let mut path = worktree_path.join(virtual_environment_name); - path.push("bin/activate"); + path.push("bin/"); + path.push(script_name); if path.exists() { return Some(path); @@ -103,6 +109,22 @@ impl Project { None } + fn activate_python_virtual_environment( + &mut self, + activate_script: Option, + terminal_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + if let Some(activate_script) = activate_script { + // Paths are not strings so we need to jump through some hoops to format the command without `format!` + let mut command = Vec::from("source ".as_bytes()); + command.extend_from_slice(activate_script.as_os_str().as_bytes()); + command.push(b'\n'); + + terminal_handle.update(cx, |this, _| this.input_bytes(command)); + } + } + pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } From 7b170304dfe1ece11ba193867c43a90050026d10 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 04:07:10 -0400 Subject: [PATCH 012/142] Shorten setting name --- assets/settings/default.json | 2 +- crates/project/src/terminals.rs | 7 +++---- crates/terminal/src/terminal.rs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f4d77f02cf5425888b5b238a2284586fefd4d09b..27be6ae5d28204eb29049ea2eaa650e55892fa4c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -348,7 +348,7 @@ // - Only works with Posix-complaint shells // - Only activates the first virtual environment it finds, regardless // of the nunber of projects in the workspace. - "automatically_activate_python_virtual_environment": false + "activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 0fa525a82ce0a754d572d9f8ccf82c9530c9fcb2..539d120e971ecb562eaaa28fdccb6d084c49bba6 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -23,9 +23,8 @@ impl Project { )); } else { let settings = settings::get::(cx); - let automatically_activate_python_virtual_environment = settings - .automatically_activate_python_virtual_environment - .clone(); + let activate_python_virtual_environment = + settings.activate_python_virtual_environment.clone(); let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( @@ -54,7 +53,7 @@ impl Project { }) .detach(); - if automatically_activate_python_virtual_environment { + if activate_python_virtual_environment { let activate_script_path = self.find_activate_script_path(&shell, cx); self.activate_python_virtual_environment( activate_script_path, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9b0f0bbc860b113de7c2e933bfdab76b25c27e02..73ff09225f62c5c09185d074163f9eea631decf1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -158,7 +158,7 @@ pub struct TerminalSettings { pub dock: TerminalDockPosition, pub default_width: f32, pub default_height: f32, - pub automatically_activate_python_virtual_environment: bool, + pub activate_python_virtual_environment: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -177,7 +177,7 @@ pub struct TerminalSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, - pub automatically_activate_python_virtual_environment: Option, + pub activate_python_virtual_environment: Option, } impl TerminalSettings { From 09fd99b1e3380f239c4971ea625a526e87b1d338 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 23 Aug 2023 15:09:15 +0200 Subject: [PATCH 013/142] moved semantic_index project intialization to queue and channel method --- crates/semantic_index/src/semantic_index.rs | 267 ++++++++------------ 1 file changed, 108 insertions(+), 159 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 79e649838a6a7418da0e9c92346e71529a003620..ffe6e74a6df05f10da53cbdf9ded64867823f941 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -15,8 +15,9 @@ use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; +use postage::stream::Stream; use postage::watch; -use project::{search::PathMatcher, Fs, Project, WorktreeId}; +use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, WorktreeId}; use smol::channel; use std::{ cmp::Ordering, @@ -96,7 +97,8 @@ struct ProjectState { subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, - queue: HashMap>, + job_queue_tx: channel::Sender, + _queue_update_task: Task<()>, } #[derive(Clone)] @@ -116,6 +118,7 @@ impl JobHandle { } impl ProjectState { fn new( + cx: &mut AppContext, subscription: gpui::Subscription, worktree_db_ids: Vec<(WorktreeId, i64)>, worktree_file_mtimes: HashMap>, @@ -125,29 +128,51 @@ impl ProjectState { let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); + let (job_queue_tx, job_queue_rx) = channel::unbounded(); + let _queue_update_task = cx.background().spawn({ + let mut worktree_queue = Vec::new(); + async move { + while let Ok(operation) = job_queue_rx.recv().await { + Self::update_queue(&mut worktree_queue, operation); + } + } + }); + Self { worktree_db_ids, worktree_file_mtimes, outstanding_job_count_rx, _outstanding_job_count_tx, subscription, - queue: HashMap::new(), + _queue_update_task, + job_queue_tx, } } - fn add_to_queue(&mut self, worktree_id: WorktreeId, operation: IndexOperation) { - if let Some(worktree_queue) = self.queue.get_mut(&worktree_id) { - worktree_queue.push(operation); - } else { - self.queue.insert(worktree_id, vec![operation]); - } + pub fn get_outstanding_count(&self) -> usize { + self.outstanding_job_count_rx.borrow().clone() } - fn pop(&mut self) -> Option { - self.queue - .iter_mut() - .next() - .and_then(|(_, mut entry)| entry.pop()) + fn update_queue(queue: &mut Vec, operation: IndexOperation) { + match operation { + IndexOperation::FlushQueue => { + for op in queue.pop() { + match op { + IndexOperation::IndexFile { payload, tx } => { + tx.try_send(payload); + } + IndexOperation::DeleteFile { payload, tx } => { + tx.try_send(payload); + } + _ => {} + } + } + } + _ => { + // TODO: This has to accomodate for duplicate files to index. + queue.push(operation); + } + } } fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option { @@ -185,10 +210,16 @@ pub struct PendingFile { job_handle: JobHandle, } -#[derive(Clone)] enum IndexOperation { - IndexFile { file: PendingFile }, - DeleteFile { file: PendingFile }, + IndexFile { + payload: PendingFile, + tx: channel::Sender, + }, + DeleteFile { + payload: DbOperation, + tx: channel::Sender, + }, + FlushQueue, } pub struct SearchResult { @@ -621,6 +652,52 @@ impl SemanticIndex { }) } + // pub fn project_entries_changed( + // &self, + // project: ModelHandle, + // changes: &Arc<[(Arc, ProjectEntryId, PathChange)]>, + // cx: &ModelContext, + // worktree_id: &WorktreeId, + // ) -> Result<()> { + // let parsing_files_tx = self.parsing_files_tx.clone(); + // let db_update_tx = self.db_update_tx.clone(); + // let (job_queue_tx, outstanding_job_tx, worktree_db_id) = { + // let state = self.projects.get(&project.downgrade()); + // if state.is_none() { + // return anyhow::Error(anyhow!("Project not yet initialized")); + // } + // let state = state.unwrap(); + // ( + // state.job_queue_tx.clone(), + // state._outstanding_job_count_tx, + // state.db_id_for_worktree_id(worktree_id), + // ) + // }; + + // for (path, entry_id, path_change) in changes.iter() { + // match path_change { + // PathChange::AddedOrUpdated => { + // let job_handle = JobHandle::new(&outstanding_job_tx); + // job_queue_tx.try_send(IndexOperation::IndexFile { + // payload: PendingFile { + // worktree_db_id, + // relative_path: path, + // absolute_path, + // language, + // modified_time, + // job_handle, + // }, + // tx: parsing_files_tx, + // }) + // } + // PathChange::Removed => {} + // _ => {} + // } + // } + + // Ok(()) + // } + pub fn initialize_project( &mut self, project: ModelHandle, @@ -653,6 +730,7 @@ impl SemanticIndex { }); let language_registry = self.language_registry.clone(); + let parsing_files_tx = self.parsing_files_tx.clone(); cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; @@ -686,24 +764,13 @@ impl SemanticIndex { let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); let job_count_tx_longlived = job_count_tx.clone(); - // this.update(&mut cx, |this, _| { - // let project_state = ProjectState::new( - // _subscription, - // worktree_db_ids, - // worktree_file_mtimes.clone(), - // job_count_rx.clone(), - // job_count_tx.clone(), - // ); - // this.projects.insert(project.downgrade(), project_state); - // }); let worktree_file_mtimes_all = worktree_file_mtimes.clone(); let worktree_files = cx .background() .spawn(async move { - let mut worktree_files = HashMap::new(); + let mut worktree_files = Vec::new(); for worktree in worktrees.into_iter() { - let mut candidate_files = Vec::new(); let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); @@ -730,8 +797,8 @@ impl SemanticIndex { if !already_stored { let job_handle = JobHandle::new(&job_count_tx); - candidate_files.push(IndexOperation::IndexFile { - file: PendingFile { + worktree_files.push(IndexOperation::IndexFile { + payload: PendingFile { worktree_db_id: db_ids_by_worktree_id[&worktree.id()], relative_path: path_buf, absolute_path, @@ -739,12 +806,11 @@ impl SemanticIndex { job_handle, modified_time: file.mtime, }, + tx: parsing_files_tx.clone(), }); } } } - - worktree_files.insert(worktree.id(), candidate_files); } anyhow::Ok(worktree_files) @@ -753,6 +819,7 @@ impl SemanticIndex { this.update(&mut cx, |this, cx| { let project_state = ProjectState::new( + cx, _subscription, worktree_db_ids, worktree_file_mtimes_all, @@ -761,10 +828,8 @@ impl SemanticIndex { ); if let Some(project_state) = this.projects.get_mut(&project.downgrade()) { - for (worktree_id, index_operations) in &worktree_files { - for op in index_operations { - project_state.add_to_queue(*worktree_id, op.clone()); - } + for op in worktree_files { + project_state.job_queue_tx.try_send(op); } } @@ -791,134 +856,18 @@ impl SemanticIndex { let parsing_files_tx = self.parsing_files_tx.clone(); let db_update_tx = self.db_update_tx.clone(); let job_count_rx = state.outstanding_job_count_rx.clone(); - let count = state.queue.values().map(Vec::len).sum(); + let count = state.get_outstanding_count(); + cx.spawn(|this, mut cx| async move { this.update(&mut cx, |this, cx| { - let Some(mut state) = this.projects.get_mut(&project.downgrade()) else { + let Some(state) = this.projects.get_mut(&project.downgrade()) else { return; }; - let Some(mut index_operation) = state.pop() else { return;}; - let _ = match index_operation { - IndexOperation::IndexFile { file } => { - parsing_files_tx.try_send(file); - } - IndexOperation::DeleteFile { file } => { - db_update_tx.try_send(DbOperation::Delete { - worktree_id: file.worktree_db_id, - path: file.relative_path, - }); - } - }; - }); - }) - .detach(); + state.job_queue_tx.try_send(IndexOperation::FlushQueue); + }) + }); Task::Ready(Some(Ok((count, job_count_rx)))) - - // cx.spawn(|this, mut cx| async move { - // futures::future::join_all(worktree_scans_complete).await; - - // let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; - - // let worktrees = project.read_with(&cx, |project, cx| { - // project - // .worktrees(cx) - // .map(|worktree| worktree.read(cx).snapshot()) - // .collect::>() - // }); - - // let mut worktree_file_mtimes = HashMap::new(); - // let mut db_ids_by_worktree_id = HashMap::new(); - // for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { - // let db_id = db_id?; - // db_ids_by_worktree_id.insert(worktree.id(), db_id); - // worktree_file_mtimes.insert( - // worktree.id(), - // this.read_with(&cx, |this, _| this.get_file_mtimes(db_id)) - // .await?, - // ); - // } - - // let worktree_db_ids = db_ids_by_worktree_id - // .iter() - // .map(|(a, b)| (*a, *b)) - // .collect(); - - // let (job_count_tx, job_count_rx) = watch::channel_with(0); - // let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - // this.update(&mut cx, |this, _| { - // let project_state = ProjectState::new( - // _subscription, - // worktree_db_ids, - // worktree_file_mtimes.clone(), - // job_count_rx.clone(), - // job_count_tx.clone(), - // ); - // this.projects.insert(project.downgrade(), project_state); - // }); - - // cx.background() - // .spawn(async move { - // let mut count = 0; - // for worktree in worktrees.into_iter() { - // let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); - // for file in worktree.files(false, 0) { - // let absolute_path = worktree.absolutize(&file.path); - - // if let Ok(language) = language_registry - // .language_for_file(&absolute_path, None) - // .await - // { - // if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) - // && &language.name().as_ref() != &"Markdown" - // && language - // .grammar() - // .and_then(|grammar| grammar.embedding_config.as_ref()) - // .is_none() - // { - // continue; - // } - - // let path_buf = file.path.to_path_buf(); - // let stored_mtime = file_mtimes.remove(&file.path.to_path_buf()); - // let already_stored = stored_mtime - // .map_or(false, |existing_mtime| existing_mtime == file.mtime); - - // if !already_stored { - // count += 1; - - // let job_handle = JobHandle::new(&job_count_tx); - // parsing_files_tx - // .try_send(PendingFile { - // worktree_db_id: db_ids_by_worktree_id[&worktree.id()], - // relative_path: path_buf, - // absolute_path, - // language, - // job_handle, - // modified_time: file.mtime, - // }) - // .unwrap(); - // } - // } - // } - // for file in file_mtimes.keys() { - // db_update_tx - // .try_send(DbOperation::Delete { - // worktree_id: db_ids_by_worktree_id[&worktree.id()], - // path: file.to_owned(), - // }) - // .unwrap(); - // } - // } - - // log::trace!( - // "walking worktree took {:?} milliseconds", - // t0.elapsed().as_millis() - // ); - // anyhow::Ok((count, job_count_rx)) - // }) - // .await - // }) } pub fn outstanding_job_count_rx( From 72f0efb7b78c6fe4974e839d84f6281b1f85cb43 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 12:49:13 -0400 Subject: [PATCH 014/142] v0.102.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6490337daecb788d3190a0aca7573b57f9391a1..aed2fb3ea96a459f0e44d8a5ce51181cff8f273b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9632,7 +9632,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.101.0" +version = "0.102.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fa1c67a6d051db61e639fef8beec8b48acf6ae70..a555e58679a1ea35df8238f8135d255eb2d52ac8 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.101.0" +version = "0.102.0" publish = false [lib] From af21546a431354c440b42a5d59d52adbeb01c94d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 14:19:24 -0400 Subject: [PATCH 015/142] collab 0.18.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aed2fb3ea96a459f0e44d8a5ce51181cff8f273b..6dea29746f0af0fe7eddd583ea72cc36d1254246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1409,7 +1409,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "async-tungstenite", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index fc8c1644cd7d127876f168ab2a32c17f5e2b114c..b8d0c269608c3de64af009865ece7bca60c53620 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.17.0" +version = "0.18.0" publish = false [[bin]] From 6c45be2dc40dd245508d124c4ae3f70249897b69 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 14:54:39 -0400 Subject: [PATCH 016/142] Add `docker system prune` command This will hopefully keep the system drive cleaned up so we don't run issues with not enough disk space. --- .github/workflows/publish_collab_image.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_collab_image.yml b/.github/workflows/publish_collab_image.yml index 3421409287dfdf146e745d9c34873d6f4a4e045e..b012e65841a1ab5f2e45ff0be05394f16247a64f 100644 --- a/.github/workflows/publish_collab_image.yml +++ b/.github/workflows/publish_collab_image.yml @@ -11,7 +11,7 @@ env: jobs: publish: - name: Publish collab server image + name: Publish collab server image runs-on: - self-hosted - deploy @@ -22,6 +22,9 @@ jobs: - name: Sign into DigitalOcean docker registry run: doctl registry login + - name: Prune Docker system + run: docker system prune + - name: Checkout repo uses: actions/checkout@v3 with: @@ -41,6 +44,6 @@ jobs: - name: Build docker image run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION} - + - name: Publish docker image run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION} From e42b9e910ede97b35414ca9987d5d39b340776d1 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 23 Aug 2023 22:28:30 +0200 Subject: [PATCH 017/142] fix async calls on project updated entries to ensure that all files are updating appropriately --- crates/search/src/project_search.rs | 1 + crates/semantic_index/src/semantic_index.rs | 165 +++++++++++++------- 2 files changed, 110 insertions(+), 56 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ca317c0ded71ab50ce46a1ec5f1af4a04a60991f..448735fe3c4856168713fcb75bea8ef60fd112e8 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -849,6 +849,7 @@ impl ProjectSearchView { let model = model.read(cx); project = model.project.clone(); SemanticIndex::global(cx).map(|semantic| { + dbg!("Initializing project"); semantic.update(cx, |this, cx| this.initialize_project(project.clone(), cx)); }); } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index ffe6e74a6df05f10da53cbdf9ded64867823f941..0df3a9cc8428e37825c4e982525c3934e8efa8b3 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -156,7 +156,7 @@ impl ProjectState { fn update_queue(queue: &mut Vec, operation: IndexOperation) { match operation { IndexOperation::FlushQueue => { - for op in queue.pop() { + while let Some(op) = queue.pop() { match op { IndexOperation::IndexFile { payload, tx } => { tx.try_send(payload); @@ -652,51 +652,103 @@ impl SemanticIndex { }) } - // pub fn project_entries_changed( - // &self, - // project: ModelHandle, - // changes: &Arc<[(Arc, ProjectEntryId, PathChange)]>, - // cx: &ModelContext, - // worktree_id: &WorktreeId, - // ) -> Result<()> { - // let parsing_files_tx = self.parsing_files_tx.clone(); - // let db_update_tx = self.db_update_tx.clone(); - // let (job_queue_tx, outstanding_job_tx, worktree_db_id) = { - // let state = self.projects.get(&project.downgrade()); - // if state.is_none() { - // return anyhow::Error(anyhow!("Project not yet initialized")); - // } - // let state = state.unwrap(); - // ( - // state.job_queue_tx.clone(), - // state._outstanding_job_count_tx, - // state.db_id_for_worktree_id(worktree_id), - // ) - // }; - - // for (path, entry_id, path_change) in changes.iter() { - // match path_change { - // PathChange::AddedOrUpdated => { - // let job_handle = JobHandle::new(&outstanding_job_tx); - // job_queue_tx.try_send(IndexOperation::IndexFile { - // payload: PendingFile { - // worktree_db_id, - // relative_path: path, - // absolute_path, - // language, - // modified_time, - // job_handle, - // }, - // tx: parsing_files_tx, - // }) - // } - // PathChange::Removed => {} - // _ => {} - // } - // } - - // Ok(()) - // } + fn project_entries_changed( + &self, + project: ModelHandle, + changes: Arc<[(Arc, ProjectEntryId, PathChange)]>, + cx: &mut ModelContext<'_, SemanticIndex>, + worktree_id: &WorktreeId, + ) -> Result<()> { + let parsing_files_tx = self.parsing_files_tx.clone(); + let db_update_tx = self.db_update_tx.clone(); + let (job_queue_tx, outstanding_job_tx, worktree_db_id) = { + let state = self + .projects + .get(&project.downgrade()) + .ok_or(anyhow!("Project not yet initialized"))?; + let worktree_db_id = state + .db_id_for_worktree_id(*worktree_id) + .ok_or(anyhow!("Worktree ID in Database Not Available"))?; + ( + state.job_queue_tx.clone(), + state._outstanding_job_count_tx.clone(), + worktree_db_id, + ) + }; + + let language_registry = self.language_registry.clone(); + let parsing_files_tx = parsing_files_tx.clone(); + let db_update_tx = db_update_tx.clone(); + + let worktree = project + .read(cx) + .worktree_for_id(worktree_id.clone(), cx) + .ok_or(anyhow!("Worktree not available"))? + .read(cx) + .snapshot(); + cx.spawn(|this, mut cx| async move { + let worktree = worktree.clone(); + for (path, entry_id, path_change) in changes.iter() { + let relative_path = path.to_path_buf(); + let absolute_path = worktree.absolutize(path); + + let Some(entry) = worktree.entry_for_id(*entry_id) else { + continue; + }; + if entry.is_ignored || entry.is_symlink || entry.is_external { + continue; + } + + match path_change { + PathChange::AddedOrUpdated | PathChange::Updated => { + log::trace!("File Updated: {:?}", path); + if let Ok(language) = language_registry + .language_for_file(&relative_path, None) + .await + { + if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) + && &language.name().as_ref() != &"Markdown" + && language + .grammar() + .and_then(|grammar| grammar.embedding_config.as_ref()) + .is_none() + { + continue; + } + + let job_handle = JobHandle::new(&outstanding_job_tx); + let new_operation = IndexOperation::IndexFile { + payload: PendingFile { + worktree_db_id, + relative_path, + absolute_path, + language, + modified_time: entry.mtime, + job_handle, + }, + tx: parsing_files_tx.clone(), + }; + job_queue_tx.try_send(new_operation); + } + } + PathChange::Removed => { + let new_operation = IndexOperation::DeleteFile { + payload: DbOperation::Delete { + worktree_id: worktree_db_id, + path: relative_path, + }, + tx: db_update_tx.clone(), + }; + job_queue_tx.try_send(new_operation); + } + _ => {} + } + } + }) + .detach(); + + Ok(()) + } pub fn initialize_project( &mut self, @@ -724,9 +776,8 @@ impl SemanticIndex { let _subscription = cx.subscribe(&project, |this, project, event, cx| { if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { - todo!(); - // this.project_entries_changed(project, changes, cx, worktree_id); - } + this.project_entries_changed(project, changes.clone(), cx, worktree_id); + }; }); let language_registry = self.language_registry.clone(); @@ -775,6 +826,10 @@ impl SemanticIndex { for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); + if file.is_external || file.is_ignored || file.is_symlink { + continue; + } + if let Ok(language) = language_registry .language_for_file(&absolute_path, None) .await @@ -827,10 +882,8 @@ impl SemanticIndex { job_count_tx_longlived, ); - if let Some(project_state) = this.projects.get_mut(&project.downgrade()) { - for op in worktree_files { - project_state.job_queue_tx.try_send(op); - } + for op in worktree_files { + project_state.job_queue_tx.try_send(op); } this.projects.insert(project.downgrade(), project_state); @@ -864,10 +917,10 @@ impl SemanticIndex { return; }; state.job_queue_tx.try_send(IndexOperation::FlushQueue); - }) - }); + }); - Task::Ready(Some(Ok((count, job_count_rx)))) + Ok((count, job_count_rx)) + }) } pub fn outstanding_job_count_rx( From 1320fadc30004572415a93aeb9bf2b0a88c92bb8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 24 Aug 2023 00:16:35 +0200 Subject: [PATCH 018/142] Bump rust embed (#2883) This is a follow-up to a recent patch I've submitted to this crate to improve compile time and runtime (in older versions file lookup was essentially O(n) with respect to path count, now it's O(log n)) Release Notes: - N/A --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- crates/zed/Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6dea29746f0af0fe7eddd583ea72cc36d1254246..ce3f97c2b4f3d0afc96d411c0fcd28738bc0694f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6130,9 +6130,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "6.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -6141,9 +6141,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "6.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" dependencies = [ "proc-macro2", "quote", @@ -6154,9 +6154,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "7.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" dependencies = [ "globset", "sha2 0.10.7", diff --git a/Cargo.toml b/Cargo.toml index a60b4eb610c07d4de6c5ea7a6a948486dae7eceb..f5b5994e89f2845f3578fce1d8ca832d1478200b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ postage = { version = "0.5", features = ["futures-traits"] } rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } -rust-embed = { version = "6.3", features = ["include-exclude"] } +rust-embed = { version = "8.0", features = ["include-exclude"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a555e58679a1ea35df8238f8135d255eb2d52ac8..a821e33327aa1d2a637881bc91a077f2daacf9a6 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -93,7 +93,7 @@ postage.workspace = true rand.workspace = true regex.workspace = true rsa = "0.4" -rust-embed = { version = "6.8.1" } +rust-embed.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true From 2a182b6a7b1df98ec5019bd9d8ccde2e1cd22628 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 23 Aug 2023 16:25:17 -0700 Subject: [PATCH 019/142] Tune styles and disclosable elements --- crates/collab_ui/src/collab_panel.rs | 68 +++++++++++++++++++++---- crates/theme/src/components.rs | 16 +++--- styles/src/component/icon_button.ts | 14 ++--- styles/src/style_tree/collab_panel.ts | 9 ++-- styles/src/style_tree/component_test.ts | 16 ++++-- 5 files changed, 93 insertions(+), 30 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 52711281c77f9eb6a92b64855d90a25c50615361..5623ada42dcfcb8dc9862e23039f44aa182aa232 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -55,7 +55,7 @@ struct RemoveChannel { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct ToggleCollapsed { +struct ToggleCollapse { channel_id: u64, } @@ -79,7 +79,16 @@ struct RenameChannel { channel_id: u64, } -actions!(collab_panel, [ToggleFocus, Remove, Secondary]); +actions!( + collab_panel, + [ + ToggleFocus, + Remove, + Secondary, + CollapseSelectedChannel, + ExpandSelectedChannel + ] +); impl_actions!( collab_panel, @@ -89,7 +98,7 @@ impl_actions!( InviteMembers, ManageMembers, RenameChannel, - ToggleCollapsed + ToggleCollapse ] ); @@ -113,6 +122,8 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); cx.add_action(CollabPanel::toggle_channel_collapsed); + cx.add_action(CollabPanel::collapse_selected_channel); + cx.add_action(CollabPanel::expand_selected_channel) } #[derive(Debug)] @@ -1356,7 +1367,7 @@ impl CollabPanel { .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { if can_collapse { - this.toggle_expanded(section, cx); + this.toggle_section_expanded(section, cx); } }) } @@ -1633,7 +1644,7 @@ impl CollabPanel { }) .align_children_center() .styleable_component() - .disclosable(disclosed, Box::new(ToggleCollapsed { channel_id })) + .disclosable(disclosed, Box::new(ToggleCollapse { channel_id })) .with_id(channel_id as usize) .with_style(theme.disclosure.clone()) .element() @@ -1863,6 +1874,12 @@ impl CollabPanel { OverlayPositionMode::Window }); + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + context_menu.show( position.unwrap_or_default(), if self.context_menu_on_selected { @@ -1871,6 +1888,7 @@ impl CollabPanel { gpui::elements::AnchorCorner::BottomLeft }, vec![ + ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), ContextMenuItem::Separator, ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), @@ -1950,7 +1968,7 @@ impl CollabPanel { | Section::Online | Section::Offline | Section::ChannelInvites => { - self.toggle_expanded(*section, cx); + self.toggle_section_expanded(*section, cx); } }, ListEntry::Contact { contact, calling } => { @@ -2038,7 +2056,7 @@ impl CollabPanel { } } - fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext) { + fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { self.collapsed_sections.remove(ix); } else { @@ -2047,8 +2065,37 @@ impl CollabPanel { self.update_entries(false, cx); } - fn toggle_channel_collapsed(&mut self, action: &ToggleCollapsed, cx: &mut ViewContext) { + fn collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if !self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { let channel_id = action.channel_id; + match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); @@ -2057,8 +2104,9 @@ impl CollabPanel { self.collapsed_channels.insert(ix, channel_id); } }; - self.update_entries(false, cx); + self.update_entries(true, cx); cx.notify(); + cx.focus_self(); } fn is_channel_collapsed(&self, channel: ChannelId) -> bool { @@ -2104,6 +2152,8 @@ impl CollabPanel { } fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { + self.collapsed_channels + .retain(|&channel| channel != action.channel_id); self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: Some(action.channel_id), pending_name: None, diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fc208954cf6ed7773b2f80e1e4cde540343e8d8a..8eaab91fe69e17cfa41d6874a89195db51c11721 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -4,7 +4,8 @@ use crate::{Interactive, Toggleable}; use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; -pub type ToggleIconButtonStyle = Toggleable>>; +pub type IconButtonStyle = Interactive>; +pub type ToggleIconButtonStyle = Toggleable; pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; @@ -27,17 +28,19 @@ impl ComponentExt for C { pub mod disclosure { use gpui::{ - elements::{Component, Empty, Flex, ParentElement, SafeStylable}, + elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable}, Action, Element, }; use schemars::JsonSchema; use serde_derive::Deserialize; - use super::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + use super::{action_button::Button, svg::Svg, ComponentExt, IconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { - pub button: ToggleIconButtonStyle, + pub button: IconButtonStyle, + #[serde(flatten)] + pub container: ContainerStyle, pub spacing: f32, #[serde(flatten)] content: S, @@ -99,6 +102,7 @@ pub mod disclosure { impl Component for Disclosable> { fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { Flex::row() + .with_spacing(self.style.spacing) .with_child(if let Some(disclosed) = self.disclosed { Button::dynamic_action(self.action) .with_id(self.id) @@ -107,7 +111,6 @@ pub mod disclosure { } else { "icons/file_icons/chevron_right.svg" })) - .toggleable(disclosed) .with_style(self.style.button) .element() .into_any() @@ -119,7 +122,6 @@ pub mod disclosure { .with_width(self.style.button.button_width.unwrap()) .into_any() }) - .with_child(Empty::new().constrained().with_width(self.style.spacing)) .with_child( self.content .with_style(self.style.content) @@ -127,6 +129,8 @@ pub mod disclosure { .flex(1., true), ) .align_children_center() + .contained() + .with_style(self.style.container) .into_any() } } diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 1a2d0bcec491abdad0bc43a7c5d1599052aca622..935909afdbb714dda754f003a01ed264f1f9721c 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO } const padding = { - top: size === Button.size.Small ? 0 : 2, - bottom: size === Button.size.Small ? 0 : 2, - left: size === Button.size.Small ? 0 : 4, - right: size === Button.size.Small ? 0 : 4, + top: size === Button.size.Small ? 2 : 2, + bottom: size === Button.size.Small ? 2 : 2, + left: size === Button.size.Small ? 2 : 4, + right: size === Button.size.Small ? 2 : 4, } return interactive({ @@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO corner_radius: 6, padding: padding, margin: m, - icon_width: 14, + icon_width: 12, icon_height: 14, - button_width: 20, - button_height: 16, + button_width: size === Button.size.Small ? 16 : 20, + button_height: 14, }, state: { default: { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 5242f90c8d83fcdbf842c453f468253ec4eb0f6b..07f367c8afe077fd3d0bbca3ab424ecd07006078 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -14,6 +14,7 @@ import { indicator } from "../component/indicator" export default function contacts_panel(): any { const theme = useTheme() + const CHANNEL_SPACING = 4 as const const NAME_MARGIN = 6 as const const SPACING = 12 as const const INDENT_SIZE = 8 as const @@ -153,8 +154,8 @@ export default function contacts_panel(): any { return { ...collab_modals(), disclosure: { - button: toggleable_icon_button(theme, {}), - spacing: 4, + button: icon_button({ variant: "ghost", size: "sm" }), + spacing: CHANNEL_SPACING, }, log_in_button: interactive({ base: { @@ -198,7 +199,7 @@ export default function contacts_panel(): any { add_channel_button: header_icon_button, leave_call_button: header_icon_button, row_height: ITEM_HEIGHT, - channel_indent: INDENT_SIZE * 2, + channel_indent: INDENT_SIZE * 2 + 2, section_icon_size: 14, header_row: { ...text(layer, "sans", { size: "sm", weight: "bold" }), @@ -268,7 +269,7 @@ export default function contacts_panel(): any { channel_name: { ...text(layer, "sans", { size: "sm" }), margin: { - left: NAME_MARGIN, + left: CHANNEL_SPACING, }, }, list_empty_label_container: { diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index eadbb5c2f1750e34b4e3ba99adecd8fff575abda..e2bb0915c172d946bfd882aa768f3db54e82cb63 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -1,18 +1,26 @@ -import { toggle_label_button_style } from "../component/label_button" + import { useTheme } from "../common" import { text_button } from "../component/text_button" -import { toggleable_icon_button } from "../component/icon_button" +import { icon_button } from "../component/icon_button" import { text } from "./components" +import { toggleable } from "../element" export default function contacts_panel(): any { const theme = useTheme() return { button: text_button({}), - toggle: toggle_label_button_style({ active_color: "accent" }), + toggle: toggleable({ + base: text_button({}), + state: { + active: { + ...text_button({ color: "accent" }) + } + } + }), disclosure: { ...text(theme.lowest, "sans", "base"), - button: toggleable_icon_button(theme, {}), + button: icon_button({ variant: "ghost" }), spacing: 4, } } From 4d2f5a8e04b72d7083f43f75aaaaeb9771252f64 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 23 Aug 2023 17:47:03 -0700 Subject: [PATCH 020/142] Fix cursor and hover styles changing when dragging the mouse --- crates/gpui/src/app.rs | 26 ++++++++++++------- crates/gpui/src/app/window.rs | 9 ++++--- .../src/pane/dragged_item_receiver.rs | 6 ++++- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 076a3fdde32ac008a05869004a859a9a002a8f80..03625c80e700ca089ade1d5d06f24781d600f681 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3299,15 +3299,15 @@ impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> { 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 { - if region_id == clicked_region_id { - Some(button) - } else { - None - } - } else { - None - }, + mouse_down: !self.window.clicked_region_ids.is_empty(), + clicked: self + .window + .clicked_region_ids + .iter() + .find(|click_region_id| **click_region_id == region_id) + // If we've gotten here, there should always be a clicked region. + // But let's be defensive and return None if there isn't. + .and_then(|_| self.window.clicked_region.map(|(_, button)| button)), accessed_hovered: false, accessed_clicked: false, } @@ -3823,14 +3823,20 @@ impl<'a, T> DerefMut for Reference<'a, T> { pub struct MouseState { pub(crate) hovered: bool, pub(crate) clicked: Option, + pub(crate) mouse_down: bool, pub(crate) accessed_hovered: bool, pub(crate) accessed_clicked: bool, } impl MouseState { + pub fn dragging(&mut self) -> bool { + self.accessed_hovered = true; + self.hovered && self.mouse_down + } + pub fn hovered(&mut self) -> bool { self.accessed_hovered = true; - self.hovered + self.hovered && (!self.mouse_down || self.clicked.is_some()) } pub fn clicked(&mut self) -> Option { diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 5638699565fbbb8c725bf3e119b3ffa0f1e07ce4..543b100284041c0e32eca992166c05db347d03c4 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -617,10 +617,11 @@ impl<'a> WindowContext<'a> { } } - if self - .window - .platform_window - .is_topmost_for_position(*position) + if pressed_button.is_none() + && self + .window + .platform_window + .is_topmost_for_position(*position) { self.platform().set_cursor_style(style_to_assign); } diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 5e487b49b4ba3423d7dca085497f7f33ae3eee60..bbe391b5b543dbc698385c10d34f4f6f8e946505 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -42,7 +42,11 @@ where 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 }; + let drag_position = if state.dragging() { + drag_position + } else { + None + }; Stack::new() .with_child(render_child(state, cx)) .with_children(drag_position.map(|drag_position| { From ff75d1663b4364fc288e76790422477e1895dc49 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 23 Aug 2023 18:22:48 -0700 Subject: [PATCH 021/142] Fix stuck click styling when dragging off of a button --- crates/gpui/src/app/window.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 543b100284041c0e32eca992166c05db347d03c4..4b8b0534d53ae70c16093125b6afa7bbd38b5c7c 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -788,6 +788,11 @@ impl<'a> WindowContext<'a> { .contains_point(self.window.mouse_position) { valid_regions.push(mouse_region.clone()); + } else { + // Let the view know that it hasn't been clicked anymore + if mouse_region.notify_on_click { + notified_views.insert(mouse_region.id().view_id()); + } } } } From 29e43384f020f7e20c3012a1f46c53a717941297 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 20 Aug 2023 14:25:19 -0700 Subject: [PATCH 022/142] Simplify macro for running a test with both databases --- crates/collab/src/db/db_tests.rs | 1014 +++++++++++++++--------------- crates/collab/src/db/test_db.rs | 17 + 2 files changed, 526 insertions(+), 505 deletions(-) diff --git a/crates/collab/src/db/db_tests.rs b/crates/collab/src/db/db_tests.rs index 8e9a80dbabfa50c580e3bc296315cf7993e6ad40..8c5dab77bd78e18c516ef064ef1d63952610ac14 100644 --- a/crates/collab/src/db/db_tests.rs +++ b/crates/collab/src/db/db_tests.rs @@ -1,242 +1,234 @@ use super::*; +use crate::test_both_dbs; use gpui::executor::{Background, Deterministic}; use pretty_assertions::{assert_eq, assert_ne}; use std::sync::Arc; use test_db::TestDb; -macro_rules! test_both_dbs { - ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => { - #[gpui::test] - async fn $postgres_test_name() { - let test_db = TestDb::postgres(Deterministic::new(0).build_background()); - let $db = test_db.db(); - $body - } - - #[gpui::test] - async fn $sqlite_test_name() { - let test_db = TestDb::sqlite(Deterministic::new(0).build_background()); - let $db = test_db.db(); - $body - } - }; -} - test_both_dbs!( + test_get_users, test_get_users_by_ids_postgres, - test_get_users_by_ids_sqlite, - db, - { - let mut user_ids = Vec::new(); - let mut user_metric_ids = Vec::new(); - for i in 1..=4 { - let user = db - .create_user( - &format!("user{i}@example.com"), - false, - NewUserParams { - github_login: format!("user{i}"), - github_user_id: i, - invite_count: 0, - }, - ) - .await - .unwrap(); - user_ids.push(user.user_id); - user_metric_ids.push(user.metrics_id); - } - - assert_eq!( - db.get_users_by_ids(user_ids.clone()).await.unwrap(), - vec![ - User { - id: user_ids[0], - github_login: "user1".to_string(), - github_user_id: Some(1), - email_address: Some("user1@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[0].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[1], - github_login: "user2".to_string(), - github_user_id: Some(2), - email_address: Some("user2@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[1].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[2], - github_login: "user3".to_string(), - github_user_id: Some(3), - email_address: Some("user3@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[2].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[3], - github_login: "user4".to_string(), - github_user_id: Some(4), - email_address: Some("user4@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[3].parse().unwrap(), - ..Default::default() - } - ] - ); - } + test_get_users_by_ids_sqlite ); -test_both_dbs!( - test_get_or_create_user_by_github_account_postgres, - test_get_or_create_user_by_github_account_sqlite, - db, - { - let user_id1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "login1".into(), - github_user_id: 101, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_id2 = db +async fn test_get_users(db: &Arc) { + let mut user_ids = Vec::new(); + let mut user_metric_ids = Vec::new(); + for i in 1..=4 { + let user = db .create_user( - "user2@example.com", + &format!("user{i}@example.com"), false, NewUserParams { - github_login: "login2".into(), - github_user_id: 102, + github_login: format!("user{i}"), + github_user_id: i, invite_count: 0, }, ) .await - .unwrap() - .user_id; - - let user = db - .get_or_create_user_by_github_account("login1", None, None) - .await - .unwrap() .unwrap(); - assert_eq!(user.id, user_id1); - assert_eq!(&user.github_login, "login1"); - assert_eq!(user.github_user_id, Some(101)); - - assert!(db - .get_or_create_user_by_github_account("non-existent-login", None, None) - .await - .unwrap() - .is_none()); + user_ids.push(user.user_id); + user_metric_ids.push(user.metrics_id); + } - let user = db - .get_or_create_user_by_github_account("the-new-login2", Some(102), None) - .await - .unwrap() - .unwrap(); - assert_eq!(user.id, user_id2); - assert_eq!(&user.github_login, "the-new-login2"); - assert_eq!(user.github_user_id, Some(102)); + assert_eq!( + db.get_users_by_ids(user_ids.clone()).await.unwrap(), + vec![ + User { + id: user_ids[0], + github_login: "user1".to_string(), + github_user_id: Some(1), + email_address: Some("user1@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[0].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[1], + github_login: "user2".to_string(), + github_user_id: Some(2), + email_address: Some("user2@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[1].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[2], + github_login: "user3".to_string(), + github_user_id: Some(3), + email_address: Some("user3@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[2].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[3], + github_login: "user4".to_string(), + github_user_id: Some(4), + email_address: Some("user4@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[3].parse().unwrap(), + ..Default::default() + } + ] + ); +} - let user = db - .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) - .await - .unwrap() - .unwrap(); - assert_eq!(&user.github_login, "login3"); - assert_eq!(user.github_user_id, Some(103)); - assert_eq!(user.email_address, Some("user3@example.com".into())); - } +test_both_dbs!( + test_get_or_create_user_by_github_account, + test_get_or_create_user_by_github_account_postgres, + test_get_or_create_user_by_github_account_sqlite ); +async fn test_get_or_create_user_by_github_account(db: &Arc) { + let user_id1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "login1".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_id2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "login2".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user = db + .get_or_create_user_by_github_account("login1", None, None) + .await + .unwrap() + .unwrap(); + assert_eq!(user.id, user_id1); + assert_eq!(&user.github_login, "login1"); + assert_eq!(user.github_user_id, Some(101)); + + assert!(db + .get_or_create_user_by_github_account("non-existent-login", None, None) + .await + .unwrap() + .is_none()); + + let user = db + .get_or_create_user_by_github_account("the-new-login2", Some(102), None) + .await + .unwrap() + .unwrap(); + assert_eq!(user.id, user_id2); + assert_eq!(&user.github_login, "the-new-login2"); + assert_eq!(user.github_user_id, Some(102)); + + let user = db + .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) + .await + .unwrap() + .unwrap(); + assert_eq!(&user.github_login, "login3"); + assert_eq!(user.github_user_id, Some(103)); + assert_eq!(user.email_address, Some("user3@example.com".into())); +} + test_both_dbs!( + test_create_access_tokens, test_create_access_tokens_postgres, - test_create_access_tokens_sqlite, - db, - { - let user = db - .create_user( - "u1@example.com", - false, - NewUserParams { - github_login: "u1".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); - let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_1).await.unwrap(), - access_token::Model { - id: token_1, - user_id: user, - hash: "h1".into(), - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); + test_create_access_tokens_sqlite +); - let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); - assert!(db.get_access_token(token_1).await.is_err()); - - let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_4).await.unwrap(), - access_token::Model { - id: token_4, - user_id: user, - hash: "h4".into() - } - ); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert!(db.get_access_token(token_2).await.is_err()); - assert!(db.get_access_token(token_1).await.is_err()); - } +async fn test_create_access_tokens(db: &Arc) { + let user = db + .create_user( + "u1@example.com", + false, + NewUserParams { + github_login: "u1".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); + let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_1).await.unwrap(), + access_token::Model { + id: token_1, + user_id: user, + hash: "h1".into(), + } + ); + assert_eq!( + db.get_access_token(token_2).await.unwrap(), + access_token::Model { + id: token_2, + user_id: user, + hash: "h2".into() + } + ); + + let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user, + hash: "h3".into() + } + ); + assert_eq!( + db.get_access_token(token_2).await.unwrap(), + access_token::Model { + id: token_2, + user_id: user, + hash: "h2".into() + } + ); + assert!(db.get_access_token(token_1).await.is_err()); + + let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_4).await.unwrap(), + access_token::Model { + id: token_4, + user_id: user, + hash: "h4".into() + } + ); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user, + hash: "h3".into() + } + ); + assert!(db.get_access_token(token_2).await.is_err()); + assert!(db.get_access_token(token_1).await.is_err()); +} + +test_both_dbs!( + test_add_contacts, + test_add_contacts_postgres, + test_add_contacts_sqlite ); -test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, { +async fn test_add_contacts(db: &Arc) { let mut user_ids = Vec::new(); for i in 0..3 { user_ids.push( @@ -403,9 +395,15 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, { busy: false, }], ); -}); +} -test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, { +test_both_dbs!( + test_metrics_id, + test_metrics_id_postgres, + test_metrics_id_sqlite +); + +async fn test_metrics_id(db: &Arc) { let NewUserResult { user_id: user1, metrics_id: metrics_id1, @@ -444,82 +442,83 @@ test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, { assert_eq!(metrics_id1.len(), 36); assert_eq!(metrics_id2.len(), 36); assert_ne!(metrics_id1, metrics_id2); -}); +} test_both_dbs!( + test_project_count, test_project_count_postgres, - test_project_count_sqlite, - db, - { - let owner_id = db.create_server("test").await.unwrap().0 as u32; + test_project_count_sqlite +); - let user1 = db - .create_user( - &format!("admin@example.com"), - true, - NewUserParams { - github_login: "admin".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user2 = db - .create_user( - &format!("user@example.com"), - false, - NewUserParams { - github_login: "user".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap(); +async fn test_project_count(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; - let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") - .await - .unwrap() - .id, - ); - db.call( - room_id, - user1.user_id, - ConnectionId { owner_id, id: 0 }, - user2.user_id, - None, + let user1 = db + .create_user( + &format!("admin@example.com"), + true, + NewUserParams { + github_login: "admin".into(), + github_user_id: 0, + invite_count: 0, + }, + ) + .await + .unwrap(); + let user2 = db + .create_user( + &format!("user@example.com"), + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, ) .await .unwrap(); - db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + let room_id = RoomId::from_proto( + db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); + .unwrap() + .id, + ); + db.call( + room_id, + user1.user_id, + ConnectionId { owner_id, id: 0 }, + user2.user_id, + None, + ) + .await + .unwrap(); + db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); - // Projects shared by admins aren't counted. - db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); - db.leave_room(ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - } -); + // Projects shared by admins aren't counted. + db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + + db.leave_room(ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); +} #[test] fn test_fuzzy_like_string() { @@ -878,7 +877,9 @@ async fn test_invite_codes() { assert!(db.has_contact(user5, user1).await.unwrap()); } -test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { +test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); + +async fn test_channels(db: &Arc) { let a_id = db .create_user( "user1@example.com", @@ -1063,267 +1064,270 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); -}); +} test_both_dbs!( + test_joining_channels, test_joining_channels_postgres, - test_joining_channels_sqlite, - db, - { - let owner_id = db.create_server("test").await.unwrap().0 as u32; + test_joining_channels_sqlite +); - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; +async fn test_joining_channels(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; - let channel_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - // can join a room with membership to its channel - let joined_room = db - .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(joined_room.room.participants.len(), 1); + let channel_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); - drop(joined_room); - // cannot join a room without membership to its channel - assert!(db - .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) - .await - .is_err()); - } -); + // can join a room with membership to its channel + let joined_room = db + .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(joined_room.room.participants.len(), 1); + + drop(joined_room); + // cannot join a room without membership to its channel + assert!(db + .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) + .await + .is_err()); +} test_both_dbs!( + test_channel_invites, test_channel_invites_postgres, - test_channel_invites_sqlite, - db, - { - db.create_server("test").await.unwrap(); + test_channel_invites_sqlite +); - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; +async fn test_channel_invites(db: &Arc) { + db.create_server("test").await.unwrap(); - let user_3 = db - .create_user( - "user3@example.com", - false, - NewUserParams { - github_login: "user3".into(), - github_user_id: 7, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - let channel_1_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); + let user_3 = db + .create_user( + "user3@example.com", + false, + NewUserParams { + github_login: "user3".into(), + github_user_id: 7, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - let channel_1_2 = db - .create_root_channel("channel_2", "2", user_1) - .await - .unwrap(); + let channel_1_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); - db.invite_channel_member(channel_1_1, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_2, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, true) - .await - .unwrap(); + let channel_1_2 = db + .create_root_channel("channel_2", "2", user_1) + .await + .unwrap(); - let user_2_invites = db - .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); + db.invite_channel_member(channel_1_1, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_2, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_1, user_3, user_1, true) + .await + .unwrap(); - assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); + let user_2_invites = db + .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); - let user_3_invites = db - .get_channel_invites_for_user(user_3) // -> [channel_1_1] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); + assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); - assert_eq!(user_3_invites, &[channel_1_1]); + let user_3_invites = db + .get_channel_invites_for_user(user_3) // -> [channel_1_1] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); - let members = db - .get_channel_member_details(channel_1_1, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: false, - }, - proto::ChannelMember { - user_id: user_3.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: true, - }, - ] - ); + assert_eq!(user_3_invites, &[channel_1_1]); - db.respond_to_channel_invite(channel_1_1, user_2, true) - .await - .unwrap(); + let members = db + .get_channel_member_details(channel_1_1, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: false, + }, + proto::ChannelMember { + user_id: user_3.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: true, + }, + ] + ); - let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), "1", user_1) - .await - .unwrap(); + db.respond_to_channel_invite(channel_1_1, user_2, true) + .await + .unwrap(); - let members = db - .get_channel_member_details(channel_1_3, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - admin: false, - }, - ] - ); - } -); + let channel_1_3 = db + .create_channel("channel_3", Some(channel_1_1), "1", user_1) + .await + .unwrap(); + + let members = db + .get_channel_member_details(channel_1_3, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + admin: false, + }, + ] + ); +} test_both_dbs!( + test_channel_renames, test_channel_renames_postgres, - test_channel_renames_sqlite, - db, - { - db.create_server("test").await.unwrap(); + test_channel_renames_sqlite +); - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; +async fn test_channel_renames(db: &Arc) { + db.create_server("test").await.unwrap(); - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; - db.rename_channel(zed_id, user_1, "#zed-archive") - .await - .unwrap(); + let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); - let zed_archive_id = zed_id; + db.rename_channel(zed_id, user_1, "#zed-archive") + .await + .unwrap(); - let (channel, _) = db - .get_channel(zed_archive_id, user_1) - .await - .unwrap() - .unwrap(); - assert_eq!(channel.name, "zed-archive"); + let zed_archive_id = zed_id; - let non_permissioned_rename = db - .rename_channel(zed_archive_id, user_2, "hacked-lol") - .await; - assert!(non_permissioned_rename.is_err()); + let (channel, _) = db + .get_channel(zed_archive_id, user_1) + .await + .unwrap() + .unwrap(); + assert_eq!(channel.name, "zed-archive"); - let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; - assert!(bad_name_rename.is_err()) - } -); + let non_permissioned_rename = db + .rename_channel(zed_archive_id, user_2, "hacked-lol") + .await; + assert!(non_permissioned_rename.is_err()); + + let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; + assert!(bad_name_rename.is_err()) +} #[gpui::test] async fn test_multiple_signup_overwrite() { diff --git a/crates/collab/src/db/test_db.rs b/crates/collab/src/db/test_db.rs index 064f85c7007fed04c23de5b7c7dadc246294ab29..40013d5b03f622c4ad9a606c4c3398a984bbb690 100644 --- a/crates/collab/src/db/test_db.rs +++ b/crates/collab/src/db/test_db.rs @@ -91,6 +91,23 @@ impl TestDb { } } +#[macro_export] +macro_rules! test_both_dbs { + ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { + #[gpui::test] + async fn $postgres_test_name() { + let test_db = TestDb::postgres(Deterministic::new(0).build_background()); + $test_name(test_db.db()).await; + } + + #[gpui::test] + async fn $sqlite_test_name() { + let test_db = TestDb::sqlite(Deterministic::new(0).build_background()); + $test_name(test_db.db()).await; + } + }; +} + impl Drop for TestDb { fn drop(&mut self) { let db = self.db.take().unwrap(); From ff5035ea3761026deadc595e483fd1bd8057230c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Aug 2023 12:00:43 -0700 Subject: [PATCH 023/142] Start work on storing channel buffers --- Cargo.lock | 2 + Cargo.toml | 1 + crates/collab/Cargo.toml | 2 + .../20221109000000_test_schema.sql | 27 +- .../20230819154600_add_channel_buffers.sql | 25 ++ crates/collab/src/db.rs | 2 + crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries.rs | 4 + crates/collab/src/db/queries/buffer_tests.rs | 41 +++ crates/collab/src/db/queries/buffers.rs | 271 ++++++++++++++++++ crates/collab/src/db/tables.rs | 3 + crates/collab/src/db/tables/buffer.rs | 32 +++ .../collab/src/db/tables/buffer_operation.rs | 37 +++ .../collab/src/db/tables/buffer_snapshot.rs | 30 ++ crates/collab/src/db/test_db.rs | 8 +- crates/rpc/Cargo.toml | 2 +- 16 files changed, 484 insertions(+), 4 deletions(-) create mode 100644 crates/collab/migrations/20230819154600_add_channel_buffers.sql create mode 100644 crates/collab/src/db/queries/buffer_tests.rs create mode 100644 crates/collab/src/db/queries/buffers.rs create mode 100644 crates/collab/src/db/tables/buffer.rs create mode 100644 crates/collab/src/db/tables/buffer_operation.rs create mode 100644 crates/collab/src/db/tables/buffer_snapshot.rs diff --git a/Cargo.lock b/Cargo.lock index 101a495b6ec1d4eb7efdec1701ee2e35e7c4619c..b10d8730fb4cfa221f2f359bf17d28a8da83cf83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1444,6 +1444,7 @@ dependencies = [ "pretty_assertions", "project", "prometheus", + "prost 0.8.0", "rand 0.8.5", "reqwest", "rpc", @@ -1456,6 +1457,7 @@ dependencies = [ "settings", "sha-1 0.9.8", "sqlx", + "text", "theme", "time 0.3.24", "tokio", diff --git a/Cargo.toml b/Cargo.toml index cd15a72366e4d1ceb7f6b18bf6529c3b0ce6b507..a35b3eea2340de9ae221cb8cdf06e06eabdc7d58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = { version = "2.1.1" } parking_lot = { version = "0.11.1" } postage = { version = "0.5", features = ["futures-traits"] } +prost = { version = "0.8" } rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index b8d0c269608c3de64af009865ece7bca60c53620..49d17bdc63821eb82e29617943e76158098cd4a9 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -16,6 +16,7 @@ required-features = ["seed-support"] [dependencies] collections = { path = "../collections" } live_kit_server = { path = "../live_kit_server" } +text = { path = "../text" } rpc = { path = "../rpc" } util = { path = "../util" } @@ -35,6 +36,7 @@ log.workspace = true nanoid = "0.4" parking_lot.workspace = true prometheus = "0.13" +prost.workspace = true rand.workspace = true reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 3dceaecef4e15a3fcbc221102110ee441b876832..1e4663a6f63d990e774bd6047a1b4797a9f30fc1 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -189,7 +189,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT now + "created_at" TIMESTAMP NOT NULL DEFAULT now, + "main_buffer_id" INTEGER REFERENCES buffers (id) ); CREATE TABLE "channel_paths" ( @@ -208,3 +209,27 @@ CREATE TABLE "channel_members" ( ); CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); + +CREATE TABLE "buffers" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "epoch" INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE "buffer_operations" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "local_timestamp" INTEGER NOT NULL, + "version" BLOB NOT NULL, + "is_undo" BOOLEAN NOT NULL, + "value" BLOB NOT NULL, + PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) +); + +CREATE TABLE "buffer_snapshots" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "text" TEXT NOT NULL, + PRIMARY KEY(buffer_id, epoch) +); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql new file mode 100644 index 0000000000000000000000000000000000000000..a4d936fd74bdd674b115109cb4640a4159699b4b --- /dev/null +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -0,0 +1,25 @@ +CREATE TABLE "buffers" ( + "id" SERIAL PRIMARY KEY, + "epoch" INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE "buffer_operations" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + "local_timestamp" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "version" BYTEA NOT NULL, + "is_undo" BOOLEAN NOT NULL, + "value" BYTEA NOT NULL, + PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) +); + +CREATE TABLE "buffer_snapshots" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "text" TEXT NOT NULL, + PRIMARY KEY(buffer_id, epoch) +); + +ALTER TABLE "channels" ADD COLUMN "main_buffer_id" INTEGER REFERENCES buffers (id); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index d322b0358936932f6df8222a558cf9e4e34272a0..19915777dc76922d92fc7f4b09d41033b0dc9213 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -52,6 +52,8 @@ pub struct Database { runtime: Option, } +// The `Database` type has so many methods that its impl blocks are split into +// separate files in the `queries` folder. impl Database { pub async fn new(options: ConnectOptions, executor: Executor) -> Result { Ok(Self { diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 514c973dad9ea5e42423de3ebdf0df271f949f78..54f9463ccac62aab39fb1e25afd68a9a2763e98c 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -110,6 +110,7 @@ fn value_to_integer(v: Value) -> Result { } } +id_type!(BufferId); id_type!(AccessTokenId); id_type!(ChannelId); id_type!(ChannelMemberId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index f67bde30b8a795efc51aadaa0248571798a60710..c4a1d57eb442eb33395dab7ff65edb67f44e8843 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -1,6 +1,7 @@ use super::*; pub mod access_tokens; +pub mod buffers; pub mod channels; pub mod contacts; pub mod projects; @@ -8,3 +9,6 @@ pub mod rooms; pub mod servers; pub mod signups; pub mod users; + +#[cfg(test)] +pub mod buffer_tests; diff --git a/crates/collab/src/db/queries/buffer_tests.rs b/crates/collab/src/db/queries/buffer_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..f0e78e1fe4a5c96b8aa5c55e6d4c400b255a07b7 --- /dev/null +++ b/crates/collab/src/db/queries/buffer_tests.rs @@ -0,0 +1,41 @@ +use super::*; +use crate::test_both_dbs; +use language::proto; +use text::Buffer; + +test_both_dbs!(test_buffers, test_buffers_postgres, test_buffers_sqlite); + +async fn test_buffers(db: &Arc) { + let buffer_id = db.create_buffer().await.unwrap(); + + let mut buffer = Buffer::new(0, 0, "".to_string()); + let mut operations = Vec::new(); + operations.push(buffer.edit([(0..0, "hello world")])); + operations.push(buffer.edit([(5..5, ", cruel")])); + operations.push(buffer.edit([(0..5, "goodbye")])); + operations.push(buffer.undo().unwrap().1); + assert_eq!(buffer.text(), "hello, cruel world"); + + let operations = operations + .into_iter() + .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) + .collect::>(); + + db.update_buffer(buffer_id, &operations).await.unwrap(); + + let buffer_data = db.get_buffer(buffer_id).await.unwrap(); + + let mut buffer_2 = Buffer::new(0, 0, buffer_data.base_text); + buffer_2 + .apply_ops(buffer_data.operations.into_iter().map(|operation| { + let operation = proto::deserialize_operation(operation).unwrap(); + if let language::Operation::Buffer(operation) = operation { + operation + } else { + unreachable!() + } + })) + .unwrap(); + + assert_eq!(buffer_2.text(), "hello, cruel world"); +} diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs new file mode 100644 index 0000000000000000000000000000000000000000..f5ff2e33679558954546a842be91439f507b62eb --- /dev/null +++ b/crates/collab/src/db/queries/buffers.rs @@ -0,0 +1,271 @@ +use super::*; +use prost::Message; + +pub struct Buffer { + pub base_text: String, + pub operations: Vec, +} + +impl Database { + pub async fn create_buffer(&self) -> Result { + self.transaction(|tx| async move { + let buffer = buffer::ActiveModel::new().insert(&*tx).await?; + Ok(buffer.id) + }) + .await + } + + pub async fn update_buffer( + &self, + buffer_id: BufferId, + operations: &[proto::Operation], + ) -> Result<()> { + self.transaction(|tx| async move { + let buffer = buffer::Entity::find_by_id(buffer_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { + match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => { + let value = + serialize_edit_operation(&operation.ranges, &operation.new_text); + let version = serialize_version(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(false), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::Undo(operation) => { + let value = serialize_undo_operation(&operation.counts); + let version = serialize_version(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(true), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::UpdateSelections(_) => None, + proto::operation::Variant::UpdateDiagnostics(_) => None, + proto::operation::Variant::UpdateCompletionTriggers(_) => None, + } + })) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_buffer(&self, id: BufferId) -> Result { + self.transaction(|tx| async move { + let buffer = buffer::Entity::find_by_id(id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + + let base_text = if buffer.epoch > 0 { + buffer_snapshot::Entity::find() + .filter( + buffer_snapshot::Column::BufferId + .eq(id) + .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such snapshot"))? + .text + } else { + String::new() + }; + + let mut rows = buffer_operation::Entity::find() + .filter( + buffer_operation::Column::BufferId + .eq(id) + .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), + ) + .stream(&*tx) + .await?; + let mut operations = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + let version = deserialize_version(&row.version)?; + let operation = if row.is_undo { + let counts = deserialize_undo_operation(&row.value)?; + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: row.replica_id as u32, + local_timestamp: row.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + counts, + }) + } else { + let (ranges, new_text) = deserialize_edit_operation(&row.value)?; + proto::operation::Variant::Edit(proto::operation::Edit { + replica_id: row.replica_id as u32, + local_timestamp: row.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + ranges, + new_text, + }) + }; + operations.push(proto::Operation { + variant: Some(operation), + }) + } + + Ok(Buffer { + base_text, + operations, + }) + }) + .await + } +} + +mod storage { + #![allow(non_snake_case)] + + use prost::Message; + + pub const VERSION: usize = 1; + + #[derive(Message)] + pub struct VectorClock { + #[prost(message, repeated, tag = "1")] + pub entries: Vec, + } + + #[derive(Message)] + pub struct VectorClockEntry { + #[prost(uint32, tag = "1")] + pub replica_id: u32, + #[prost(uint32, tag = "2")] + pub timestamp: u32, + } + + #[derive(Message)] + pub struct TextEdit { + #[prost(message, repeated, tag = "1")] + pub ranges: Vec, + #[prost(string, repeated, tag = "2")] + pub texts: Vec, + } + + #[derive(Message)] + pub struct Range { + #[prost(uint64, tag = "1")] + pub start: u64, + #[prost(uint64, tag = "2")] + pub end: u64, + } + + #[derive(Message)] + pub struct Undo { + #[prost(message, repeated, tag = "1")] + pub entries: Vec, + } + + #[derive(Message)] + pub struct UndoCount { + #[prost(uint32, tag = "1")] + pub replica_id: u32, + #[prost(uint32, tag = "2")] + pub local_timestamp: u32, + #[prost(uint32, tag = "3")] + pub count: u32, + } +} + +fn serialize_version(version: &Vec) -> Vec { + storage::VectorClock { + entries: version + .iter() + .map(|entry| storage::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect(), + } + .encode_to_vec() +} + +fn deserialize_version(bytes: &[u8]) -> Result> { + let clock = storage::VectorClock::decode(bytes).map_err(|error| anyhow!("{}", error))?; + Ok(clock + .entries + .into_iter() + .map(|entry| proto::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect()) +} + +fn serialize_edit_operation(ranges: &[proto::Range], texts: &[String]) -> Vec { + storage::TextEdit { + ranges: ranges + .iter() + .map(|range| storage::Range { + start: range.start, + end: range.end, + }) + .collect(), + texts: texts.to_vec(), + } + .encode_to_vec() +} + +fn deserialize_edit_operation(bytes: &[u8]) -> Result<(Vec, Vec)> { + let edit = storage::TextEdit::decode(bytes).map_err(|error| anyhow!("{}", error))?; + let ranges = edit + .ranges + .into_iter() + .map(|range| proto::Range { + start: range.start, + end: range.end, + }) + .collect(); + Ok((ranges, edit.texts)) +} + +fn serialize_undo_operation(counts: &Vec) -> Vec { + storage::Undo { + entries: counts + .iter() + .map(|entry| storage::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), + } + .encode_to_vec() +} + +fn deserialize_undo_operation(bytes: &[u8]) -> Result> { + let undo = storage::Undo::decode(bytes).map_err(|error| anyhow!("{}", error))?; + Ok(undo + .entries + .iter() + .map(|entry| proto::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect()) +} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index c4c7e4f312afa8bf067fa1778c9c706de0cdb3af..fbf4bff2a6efea0e467ebd7f7ff527fc18f1144b 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -1,4 +1,7 @@ pub mod access_token; +pub mod buffer; +pub mod buffer_operation; +pub mod buffer_snapshot; pub mod channel; pub mod channel_member; pub mod channel_path; diff --git a/crates/collab/src/db/tables/buffer.rs b/crates/collab/src/db/tables/buffer.rs new file mode 100644 index 0000000000000000000000000000000000000000..84e62cc0712716dd6a6c2c58f152e26a9553fe69 --- /dev/null +++ b/crates/collab/src/db/tables/buffer.rs @@ -0,0 +1,32 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffers")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: BufferId, + pub epoch: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::buffer_operation::Entity")] + Operations, + #[sea_orm(has_many = "super::buffer_snapshot::Entity")] + Snapshots, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Operations.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Snapshots.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/buffer_operation.rs b/crates/collab/src/db/tables/buffer_operation.rs new file mode 100644 index 0000000000000000000000000000000000000000..59626c1e77f2ba80b828a5cede6a8dcbfc8dbefe --- /dev/null +++ b/crates/collab/src/db/tables/buffer_operation.rs @@ -0,0 +1,37 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffer_operations")] +pub struct Model { + #[sea_orm(primary_key)] + pub buffer_id: BufferId, + #[sea_orm(primary_key)] + pub epoch: i32, + #[sea_orm(primary_key)] + pub lamport_timestamp: i32, + #[sea_orm(primary_key)] + pub replica_id: i32, + pub local_timestamp: i32, + pub version: Vec, + pub is_undo: bool, + pub value: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/buffer_snapshot.rs b/crates/collab/src/db/tables/buffer_snapshot.rs new file mode 100644 index 0000000000000000000000000000000000000000..ca8712a053db2190a8280e10d8313dec92099adf --- /dev/null +++ b/crates/collab/src/db/tables/buffer_snapshot.rs @@ -0,0 +1,30 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffer_snapshots")] +pub struct Model { + #[sea_orm(primary_key)] + pub buffer_id: BufferId, + #[sea_orm(primary_key)] + pub epoch: i32, + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/test_db.rs b/crates/collab/src/db/test_db.rs index 40013d5b03f622c4ad9a606c4c3398a984bbb690..71e352eb864392f2384f8dbd16e0cffdea8af84c 100644 --- a/crates/collab/src/db/test_db.rs +++ b/crates/collab/src/db/test_db.rs @@ -96,13 +96,17 @@ macro_rules! test_both_dbs { ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { #[gpui::test] async fn $postgres_test_name() { - let test_db = TestDb::postgres(Deterministic::new(0).build_background()); + let test_db = crate::db::test_db::TestDb::postgres( + gpui::executor::Deterministic::new(0).build_background(), + ); $test_name(test_db.db()).await; } #[gpui::test] async fn $sqlite_test_name() { - let test_db = TestDb::sqlite(Deterministic::new(0).build_background()); + let test_db = crate::db::test_db::TestDb::sqlite( + gpui::executor::Deterministic::new(0).build_background(), + ); $test_name(test_db.db()).await; } }; diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 008fa9c316e78c0e79e6cc7170fe5a7d837c5de3..3c307be4fbd299bb0a5fa65813da56f69a77641b 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -23,7 +23,7 @@ async-tungstenite = "0.16" base64 = "0.13" futures.workspace = true parking_lot.workspace = true -prost = "0.8" +prost.workspace = true rand.workspace = true rsa = "0.4" serde.workspace = true From a7a4e2e3699659af9e8bbfd232faa3608aae97ea Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 21 Aug 2023 16:30:57 -0700 Subject: [PATCH 024/142] Add buffer integration test Rearrange channel crate structure Get channel buffer from database co-authored-by: Max --- Cargo.lock | 41 +++++++++ Cargo.toml | 1 + crates/call/Cargo.toml | 1 + crates/call/src/call.rs | 5 +- crates/channel/Cargo.toml | 51 +++++++++++ crates/channel/src/channel.rs | 7 ++ crates/channel/src/channel_buffer.rs | 80 ++++++++++++++++++ .../{client => channel}/src/channel_store.rs | 6 +- .../src/channel_store_tests.rs | 3 + crates/client/Cargo.toml | 1 + crates/client/src/client.rs | 5 -- crates/client/src/user.rs | 4 +- crates/collab/Cargo.toml | 1 + crates/collab/src/db.rs | 5 +- crates/collab/src/db/queries.rs | 3 - crates/collab/src/db/queries/channels.rs | 28 +++++++ crates/collab/src/db/tables/channel.rs | 3 +- crates/collab/src/db/{test_db.rs => tests.rs} | 10 ++- .../src/db/{queries => tests}/buffer_tests.rs | 0 crates/collab/src/db/{ => tests}/db_tests.rs | 31 ++++++- crates/collab/src/rpc.rs | 25 +++++- crates/collab/src/tests.rs | 7 +- .../collab/src/tests/channel_buffer_tests.rs | 84 +++++++++++++++++++ crates/collab/src/tests/channel_tests.rs | 3 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/collab_panel.rs | 6 +- .../src/collab_panel/channel_modal.rs | 3 +- crates/rpc/proto/zed.proto | 12 +++ crates/rpc/src/proto.rs | 5 +- crates/workspace/Cargo.toml | 1 + crates/workspace/src/workspace.rs | 3 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 5 +- 33 files changed, 403 insertions(+), 39 deletions(-) create mode 100644 crates/channel/Cargo.toml create mode 100644 crates/channel/src/channel.rs create mode 100644 crates/channel/src/channel_buffer.rs rename crates/{client => channel}/src/channel_store.rs (99%) rename crates/{client => channel}/src/channel_store_tests.rs (98%) rename crates/collab/src/db/{test_db.rs => tests.rs} (95%) rename crates/collab/src/db/{queries => tests}/buffer_tests.rs (100%) rename crates/collab/src/db/{ => tests}/db_tests.rs (98%) create mode 100644 crates/collab/src/tests/channel_buffer_tests.rs diff --git a/Cargo.lock b/Cargo.lock index b10d8730fb4cfa221f2f359bf17d28a8da83cf83..a40aa7d89ce879c7be11856321e0a4bb5815ef23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1063,6 +1063,7 @@ dependencies = [ "anyhow", "async-broadcast", "audio", + "channel", "client", "collections", "fs", @@ -1190,6 +1191,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "channel" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "db", + "futures 0.3.28", + "gpui", + "image", + "language", + "lazy_static", + "log", + "parking_lot 0.11.2", + "postage", + "rand 0.8.5", + "rpc", + "schemars", + "serde", + "serde_derive", + "settings", + "smol", + "staff_mode", + "sum_tree", + "tempfile", + "text", + "thiserror", + "time 0.3.24", + "tiny_http", + "url", + "util", + "uuid 1.4.1", +] + [[package]] name = "chrono" version = "0.4.26" @@ -1354,6 +1390,7 @@ dependencies = [ "staff_mode", "sum_tree", "tempfile", + "text", "thiserror", "time 0.3.24", "tiny_http", @@ -1418,6 +1455,7 @@ dependencies = [ "axum-extra", "base64 0.13.1", "call", + "channel", "clap 3.2.25", "client", "collections", @@ -1480,6 +1518,7 @@ dependencies = [ "anyhow", "auto_update", "call", + "channel", "client", "clock", "collections", @@ -9536,6 +9575,7 @@ dependencies = [ "async-recursion 1.0.4", "bincode", "call", + "channel", "client", "collections", "context_menu", @@ -9661,6 +9701,7 @@ dependencies = [ "backtrace", "breadcrumbs", "call", + "channel", "chrono", "cli", "client", diff --git a/Cargo.toml b/Cargo.toml index a35b3eea2340de9ae221cb8cdf06e06eabdc7d58..0fb8f0b6b718013b65a999cd8620282fd6979a6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/auto_update", "crates/breadcrumbs", "crates/call", + "crates/channel", "crates/cli", "crates/client", "crates/clock", diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index eb448d8d8d089369c724f49e5911a8946598f8a4..b4e94fe56c3b12533d232eacf30c5edd633d5a03 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -20,6 +20,7 @@ test-support = [ [dependencies] audio = { path = "../audio" } +channel = { path = "../channel" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 5fef53fa814c00b3d88c05861620b557f9f1802f..5af094df05977c4e72cd36b98445588686f4e896 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -7,9 +7,8 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use client::{ - proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, -}; +use channel::ChannelId; +use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; use collections::HashSet; use futures::{future::Shared, FutureExt}; use postage::watch; diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..0978462a1a8a8a66760992edc4967b5b451603bc --- /dev/null +++ b/crates/channel/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "channel" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/channel.rs" +doctest = false + +[features] +test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] + +[dependencies] +client = { path = "../client" } +collections = { path = "../collections" } +db = { path = "../db" } +gpui = { path = "../gpui" } +util = { path = "../util" } +rpc = { path = "../rpc" } +text = { path = "../text" } +language = { path = "../language" } +settings = { path = "../settings" } +staff_mode = { path = "../staff_mode" } +sum_tree = { path = "../sum_tree" } + +anyhow.workspace = true +futures.workspace = true +image = "0.23" +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +rand.workspace = true +schemars.workspace = true +smol.workspace = true +thiserror.workspace = true +time.workspace = true +tiny_http = "0.8" +uuid = { version = "1.1.2", features = ["v4"] } +url = "2.2" +serde.workspace = true +serde_derive.workspace = true +tempfile = "3" + +[dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +rpc = { path = "../rpc", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs new file mode 100644 index 0000000000000000000000000000000000000000..67c560a1fcacea38cb3b966a9dc122fa2ecf6074 --- /dev/null +++ b/crates/channel/src/channel.rs @@ -0,0 +1,7 @@ +mod channel_store; + +pub mod channel_buffer; +pub use channel_store::*; + +#[cfg(test)] +mod channel_store_tests; diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs new file mode 100644 index 0000000000000000000000000000000000000000..10f59bce46c12146a52656312eb9e21dddb6c263 --- /dev/null +++ b/crates/channel/src/channel_buffer.rs @@ -0,0 +1,80 @@ +use crate::ChannelId; +use anyhow::Result; +use client::Client; +use gpui::{Entity, ModelContext, ModelHandle, Task}; +use rpc::proto::GetChannelBuffer; +use std::sync::Arc; + +// Open the channel document +// ChannelDocumentView { ChannelDocument, Editor } -> On clone, clones internal ChannelDocument handle, instantiates new editor +// Produces a view which is: (ChannelDocument, Editor), ChannelDocument manages subscriptions +// ChannelDocuments -> Buffers -> Editor with that buffer + +// ChannelDocuments { +// ChannleBuffers: HashMap> +// } + +pub struct ChannelBuffer { + channel_id: ChannelId, + buffer: Option>, + client: Arc, +} + +impl Entity for ChannelBuffer { + type Event = (); +} + +impl ChannelBuffer { + pub fn for_channel( + channel_id: ChannelId, + client: Arc, + cx: &mut ModelContext, + ) -> Self { + Self { + channel_id, + client, + buffer: None, + } + } + + fn on_buffer_update( + &mut self, + buffer: ModelHandle, + event: &language::Event, + cx: &mut ModelContext, + ) { + // + } + + pub fn buffer( + &mut self, + cx: &mut ModelContext, + ) -> Task>> { + if let Some(buffer) = &self.buffer { + Task::ready(Ok(buffer.clone())) + } else { + let channel_id = self.channel_id; + let client = self.client.clone(); + cx.spawn(|this, mut cx| async move { + let response = client.request(GetChannelBuffer { channel_id }).await?; + + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + this.update(&mut cx, |this, cx| { + let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx)); + buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + + cx.subscribe(&buffer, Self::on_buffer_update).detach(); + + this.buffer = Some(buffer.clone()); + anyhow::Ok(buffer) + }) + }) + } + } +} diff --git a/crates/client/src/channel_store.rs b/crates/channel/src/channel_store.rs similarity index 99% rename from crates/client/src/channel_store.rs rename to crates/channel/src/channel_store.rs index 6352ac791edaa0bf50704e4e42bf41069031be55..b9b2c98acd3d1ca1a8547112731427e69cd2bfa7 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,7 +1,8 @@ -use crate::Status; -use crate::{Client, Subscription, User, UserStore}; use anyhow::anyhow; use anyhow::Result; +use client::Status; +use client::UserId; +use client::{Client, Subscription, User, UserStore}; use collections::HashMap; use collections::HashSet; use futures::channel::mpsc; @@ -13,7 +14,6 @@ use std::sync::Arc; use util::ResultExt; pub type ChannelId = u64; -pub type UserId = u64; pub struct ChannelStore { channels_by_id: HashMap>, diff --git a/crates/client/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs similarity index 98% rename from crates/client/src/channel_store_tests.rs rename to crates/channel/src/channel_store_tests.rs index 51e819349e7c665976d4ff0af5f20c7bb32eaff2..18894b1f472f907d3b54ad35df57d78e5e974565 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -1,4 +1,7 @@ use super::*; +use client::{Client, UserStore}; +use gpui::{AppContext, ModelHandle}; +use rpc::proto; use util::http::FakeHttpClient; #[gpui::test] diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 3ecc51598696cd9ec5965c35d346bda069418086..64d8f02c8ae1eba2525abca8a4847edb30a458e8 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -17,6 +17,7 @@ db = { path = "../db" } gpui = { path = "../gpui" } util = { path = "../util" } rpc = { path = "../rpc" } +text = { path = "../text" } settings = { path = "../settings" } staff_mode = { path = "../staff_mode" } sum_tree = { path = "../sum_tree" } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 8ef3e32ea8f98b47a744e148f881289934fae215..a32c415f7e9d1b9699b51582f05bee3af06792e2 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,10 +1,6 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; -#[cfg(test)] -mod channel_store_tests; - -pub mod channel_store; pub mod telemetry; pub mod user; @@ -48,7 +44,6 @@ use util::channel::ReleaseChannel; use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; -pub use channel_store::*; pub use rpc::*; pub use telemetry::ClickhouseEvent; pub use user::*; diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index be11d1fb442b8d69228f867156b3b7e6c08b0b66..1dc384da1725c6d58b92aff7477ed516ef69590f 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -10,9 +10,11 @@ use std::sync::{Arc, Weak}; use util::http::HttpClient; use util::TryFutureExt as _; +pub type UserId = u64; + #[derive(Default, Debug)] pub struct User { - pub id: u64, + pub id: UserId, pub github_login: String, pub avatar: Option>, } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 49d17bdc63821eb82e29617943e76158098cd4a9..fc78a03f6777046dc88377515d5cd3d63fe57374 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -64,6 +64,7 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } +channel = { path = "../channel" } editor = { path = "../editor", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 19915777dc76922d92fc7f4b09d41033b0dc9213..9c759f79a8dd47f4cc950809c7816f0204273372 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,7 +1,8 @@ #[cfg(test)] -mod db_tests; +pub mod tests; + #[cfg(test)] -pub mod test_db; +pub use tests::TestDb; mod ids; mod queries; diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index c4a1d57eb442eb33395dab7ff65edb67f44e8843..09a8f073b469f72773a0220750f5d65cf85629af 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -9,6 +9,3 @@ pub mod rooms; pub mod servers; pub mod signups; pub mod users; - -#[cfg(test)] -pub mod buffer_tests; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index e3d3643a61f3b1fb1b498db9ef6ae36d1a1aa5ce..85a9304a2ec2cf7062f0a0b968692bce24d9f438 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -689,6 +689,34 @@ impl Database { }) .await } + + pub async fn get_or_create_buffer_for_channel( + &self, + channel_id: ChannelId, + ) -> Result { + self.transaction(|tx| async move { + let tx = tx; + let channel = channel::Entity::find_by_id(channel_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("invalid channel"))?; + + if let Some(id) = channel.main_buffer_id { + return Ok(id); + } else { + let buffer = buffer::ActiveModel::new().insert(&*tx).await?; + channel::ActiveModel { + id: ActiveValue::Unchanged(channel_id), + main_buffer_id: ActiveValue::Set(Some(buffer.id)), + ..Default::default() + } + .update(&*tx) + .await?; + Ok(buffer.id) + } + }) + .await + } } #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index f00b4ced627884d3d5c627ab515c61f9bdb0a53d..444d5fa6d9230faa8fc256cf38fafe55d126c6ce 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -1,4 +1,4 @@ -use crate::db::ChannelId; +use crate::db::{BufferId, ChannelId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -7,6 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, pub name: String, + pub main_buffer_id: Option, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/test_db.rs b/crates/collab/src/db/tests.rs similarity index 95% rename from crates/collab/src/db/test_db.rs rename to crates/collab/src/db/tests.rs index 71e352eb864392f2384f8dbd16e0cffdea8af84c..36a0888a62ed243904598d1386f8567fe5b821fd 100644 --- a/crates/collab/src/db/test_db.rs +++ b/crates/collab/src/db/tests.rs @@ -1,3 +1,6 @@ +mod buffer_tests; +mod db_tests; + use super::*; use gpui::executor::Background; use parking_lot::Mutex; @@ -96,7 +99,7 @@ macro_rules! test_both_dbs { ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { #[gpui::test] async fn $postgres_test_name() { - let test_db = crate::db::test_db::TestDb::postgres( + let test_db = crate::db::TestDb::postgres( gpui::executor::Deterministic::new(0).build_background(), ); $test_name(test_db.db()).await; @@ -104,9 +107,8 @@ macro_rules! test_both_dbs { #[gpui::test] async fn $sqlite_test_name() { - let test_db = crate::db::test_db::TestDb::sqlite( - gpui::executor::Deterministic::new(0).build_background(), - ); + let test_db = + crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background()); $test_name(test_db.db()).await; } }; diff --git a/crates/collab/src/db/queries/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs similarity index 100% rename from crates/collab/src/db/queries/buffer_tests.rs rename to crates/collab/src/db/tests/buffer_tests.rs diff --git a/crates/collab/src/db/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs similarity index 98% rename from crates/collab/src/db/db_tests.rs rename to crates/collab/src/db/tests/db_tests.rs index 8c5dab77bd78e18c516ef064ef1d63952610ac14..0fffabc7c43a3a77bc5e2837fc66973a78a2c028 100644 --- a/crates/collab/src/db/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -3,7 +3,7 @@ use crate::test_both_dbs; use gpui::executor::{Background, Deterministic}; use pretty_assertions::{assert_eq, assert_ne}; use std::sync::Arc; -use test_db::TestDb; +use tests::TestDb; test_both_dbs!( test_get_users, @@ -1329,6 +1329,35 @@ async fn test_channel_renames(db: &Arc) { assert!(bad_name_rename.is_err()) } +test_both_dbs!( + test_get_or_create_channel_buffer, + test_get_or_create_channel_buffer_postgres, + test_get_or_create_channel_buffer_sqlite +); + +async fn test_get_or_create_channel_buffer(db: &Arc) { + let a_id = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + let first_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap(); + let second_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap(); + + assert_eq!(first_buffer_id, second_buffer_id); +} + #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 521aa3e7b45b7be2683a4312395b8328df2892b0..22eb23ce8e1ef9684a06f6d608ef1926d78abe8f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -35,8 +35,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, + self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, GetChannelBufferResponse, + LiveKitConnectionInfo, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -248,6 +248,7 @@ impl Server { .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) .add_request_handler(rename_channel) + .add_request_handler(get_channel_buffer) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -2478,6 +2479,26 @@ async fn join_channel( Ok(()) } +async fn get_channel_buffer( + request: proto::GetChannelBuffer, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let buffer_id = db.get_or_create_buffer_for_channel(channel_id).await?; + + let buffer = db.get_buffer(buffer_id).await?; + + response.send(GetChannelBufferResponse { + base_text: buffer.base_text, + operations: buffer.operations, + })?; + + Ok(()) +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index c9f358ca5bbdb875bb054c40b605c004d460075f..831bccbb724ec02bd0ab28cea742948902c39180 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,14 +1,14 @@ use crate::{ - db::{test_db::TestDb, NewUserParams, UserId}, + db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, rpc::{Server, CLEANUP_TIMEOUT}, AppState, }; use anyhow::anyhow; use call::{ActiveCall, Room}; +use channel::ChannelStore; use client::{ - self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError, - UserStore, + self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; use collections::{HashMap, HashSet}; use fs::FakeFs; @@ -31,6 +31,7 @@ use std::{ use util::http::FakeHttpClient; use workspace::Workspace; +mod channel_buffer_tests; mod channel_tests; mod integration_tests; mod randomized_integration_tests; diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..e7f662523e5aae23b4e320bf7e28f0c1eb3146b6 --- /dev/null +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -0,0 +1,84 @@ +use crate::tests::TestServer; + +use channel::channel_buffer::ChannelBuffer; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; +use std::{ops::Range, sync::Arc}; + +#[gpui::test] +async fn test_channel_buffers( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let zed_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let a_document = + cx_a.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx)); + let channel_buffer_a = a_document + .update(cx_a, |doc, cx| doc.buffer(cx)) + .await + .unwrap(); + + edit_channel_buffer(&channel_buffer_a, cx_a, [(0..0, "hello world")]); + edit_channel_buffer(&channel_buffer_a, cx_a, [(5..5, ", cruel")]); + edit_channel_buffer(&channel_buffer_a, cx_a, [(0..5, "goodbye")]); + undo_channel_buffer(&channel_buffer_a, cx_a); + + assert_eq!( + channel_buffer_text(&channel_buffer_a, cx_a), + "hello, cruel world" + ); + + let b_document = + cx_b.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx)); + let channel_buffer_b = b_document + .update(cx_b, |doc, cx| doc.buffer(cx)) + .await + .unwrap(); + + assert_eq!( + channel_buffer_text(&channel_buffer_b, cx_b), + "hello, cruel world" + ); + + edit_channel_buffer(&channel_buffer_b, cx_b, [(7..12, "beautiful")]); + + deterministic.run_until_parked(); + + assert_eq!( + channel_buffer_text(&channel_buffer_a, cx_a), + "hello, beautiful world" + ); + assert_eq!( + channel_buffer_text(&channel_buffer_b, cx_b), + "hello, beautiful world" + ); +} + +fn edit_channel_buffer( + channel_buffer: &ModelHandle, + cx: &mut TestAppContext, + edits: I, +) where + I: IntoIterator, &'static str)>, +{ + channel_buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx)); +} + +fn undo_channel_buffer(channel_buffer: &ModelHandle, cx: &mut TestAppContext) { + channel_buffer.update(cx, |buffer, cx| buffer.undo(cx)); +} + +fn channel_buffer_text( + channel_buffer: &ModelHandle, + cx: &mut TestAppContext, +) -> String { + channel_buffer.read_with(cx, |buffer, _| buffer.text()) +} diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 06cf3607c0555a606b2409a6b419d5df12794121..41d228677227068ded7f50df3902928d7033caef 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -3,7 +3,8 @@ use crate::{ tests::{room_participants, RoomParticipants, TestServer}, }; use call::ActiveCall; -use client::{ChannelId, ChannelMembership, ChannelStore, User}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; +use client::User; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; use std::sync::Arc; diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 471608c43ec86a1afce15bc5552bdd58b7d0cd86..e0177f660909d0c6deaa07fb8b80f822f243b9dc 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -26,6 +26,7 @@ auto_update = { path = "../auto_update" } db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } +channel = { path = "../channel" } clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 5623ada42dcfcb8dc9862e23039f44aa182aa232..ab692dd1667173b2c45e47c0cec634a7f8bdb38b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -4,10 +4,8 @@ mod panel_settings; use anyhow::Result; use call::ActiveCall; -use client::{ - proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, -}; - +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; +use client::{proto::PeerId, Client, Contact, User, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 75ab40be85adb1e1df7678cf292c4c177237db0c..0adf2806d72dc5c440ee08ec80a1331cdccc62cc 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,4 +1,5 @@ -use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; +use client::{proto, User, UserId, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index caa5efd2cb10271ab3320f76cbc0e38fce257764..baeaae187622a0ca7ea5c4c43b8fd5f07cd09a46 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -142,6 +142,9 @@ message Envelope { GetChannelMembersResponse get_channel_members_response = 128; SetChannelMemberAdmin set_channel_member_admin = 129; RenameChannel rename_channel = 130; + + GetChannelBuffer get_channel_buffer = 131; + GetChannelBufferResponse get_channel_buffer_response = 132; } } @@ -948,6 +951,15 @@ message RenameChannel { string name = 2; } +message GetChannelBuffer { + uint64 channel_id = 1; +} + +message GetChannelBufferResponse { + string base_text = 1; + repeated Operation operations = 2; +} + message RespondToChannelInvite { uint64 channel_id = 1; bool accept = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 92732b00b5dc19ab16136c1ac9511a54f6d2e932..21a491b9342ce40b158333d96a4d98e20418fc70 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -248,7 +248,9 @@ messages!( (GetPrivateUserInfo, Foreground), (GetPrivateUserInfoResponse, Foreground), (GetChannelMembers, Foreground), - (GetChannelMembersResponse, Foreground) + (GetChannelMembersResponse, Foreground), + (GetChannelBuffer, Foreground), + (GetChannelBufferResponse, Foreground) ); request_messages!( @@ -315,6 +317,7 @@ request_messages!( (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), + (GetChannelBuffer, GetChannelBufferResponse) ); entity_messages!( diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 8606be4944830f9859863f1510ffe3413631b31a..e2dae07b8c9dc9c75ac60c32499a7884834b75f4 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -22,6 +22,7 @@ test-support = [ db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } +channel = { path = "../channel" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } drag_and_drop = { path = "../drag_and_drop" } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 79b701e015e0ed5a1a10d34962916fce50d3795f..a8354472aa5a26385c6919002a830aa225cbce46 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,9 +12,10 @@ mod workspace_settings; use anyhow::{anyhow, Context, Result}; use call::ActiveCall; +use channel::ChannelStore; use client::{ proto::{self, PeerId}, - ChannelStore, Client, TypedEnvelope, UserStore, + Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index faa3ae69530062ad06dabf111d2478b616498680..92900f84cb54ea9563de2618989bc4aac470f417 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -21,6 +21,7 @@ activity_indicator = { path = "../activity_indicator" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } call = { path = "../call" } +channel = { path = "../channel" } cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index caeaecededf585df32750555ab5a1bdd6a69e380..b905c1d37bc8bf040e78393c2f07d63c4eecc996 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,13 +3,12 @@ use anyhow::{anyhow, Context, Result}; use backtrace::Backtrace; +use channel::ChannelStore; use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, }; -use client::{ - self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, -}; +use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; use editor::{scroll::autoscroll::Autoscroll, Editor}; use futures::{ From 364ed1f840fc62e3dbb2da464d75cbfec2f100c0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 21 Aug 2023 17:53:37 -0700 Subject: [PATCH 025/142] WIP: pass synchronize channel buffers integration test --- crates/channel/src/channel_buffer.rs | 113 +++++++++++------- .../20221109000000_test_schema.sql | 21 +++- .../20230819154600_add_channel_buffers.sql | 18 ++- crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries/buffers.rs | 105 +++++++++++++--- crates/collab/src/db/queries/channels.rs | 28 ----- crates/collab/src/db/tables.rs | 1 + crates/collab/src/db/tables/buffer.rs | 23 +++- crates/collab/src/db/tables/channel.rs | 9 +- .../db/tables/channel_buffer_collaborator.rs | 42 +++++++ crates/collab/src/db/tests/buffer_tests.rs | 57 ++++++++- crates/collab/src/rpc.rs | 52 ++++++-- .../collab/src/tests/channel_buffer_tests.rs | 40 +++---- crates/rpc/proto/zed.proto | 25 ++-- crates/rpc/src/proto.rs | 11 +- 15 files changed, 411 insertions(+), 135 deletions(-) create mode 100644 crates/collab/src/db/tables/channel_buffer_collaborator.rs diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 10f59bce46c12146a52656312eb9e21dddb6c263..372bd319a135b47762db2cbac4d2c54f5aad0dd1 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,9 +1,10 @@ use crate::ChannelId; use anyhow::Result; use client::Client; -use gpui::{Entity, ModelContext, ModelHandle, Task}; -use rpc::proto::GetChannelBuffer; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use rpc::{proto, TypedEnvelope}; use std::sync::Arc; +use util::ResultExt; // Open the channel document // ChannelDocumentView { ChannelDocument, Editor } -> On clone, clones internal ChannelDocument handle, instantiates new editor @@ -14,9 +15,12 @@ use std::sync::Arc; // ChannleBuffers: HashMap> // } +type BufferId = u64; + pub struct ChannelBuffer { channel_id: ChannelId, - buffer: Option>, + buffer_id: BufferId, + buffer: ModelHandle, client: Arc, } @@ -28,53 +32,76 @@ impl ChannelBuffer { pub fn for_channel( channel_id: ChannelId, client: Arc, - cx: &mut ModelContext, - ) -> Self { - Self { - channel_id, - client, - buffer: None, - } - } + cx: &mut AppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let response = client + .request(proto::OpenChannelBuffer { channel_id }) + .await?; - fn on_buffer_update( - &mut self, - buffer: ModelHandle, - event: &language::Event, - cx: &mut ModelContext, - ) { - // - } + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + let buffer_id = response.buffer_id; - pub fn buffer( - &mut self, - cx: &mut ModelContext, - ) -> Task>> { - if let Some(buffer) = &self.buffer { - Task::ready(Ok(buffer.clone())) - } else { - let channel_id = self.channel_id; - let client = self.client.clone(); - cx.spawn(|this, mut cx| async move { - let response = client.request(GetChannelBuffer { channel_id }).await?; + let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx)); + buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; - let base_text = response.base_text; - let operations = response - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::, _>>()?; + anyhow::Ok(cx.add_model(|cx| { + cx.subscribe(&buffer, Self::on_buffer_update).detach(); + client.add_model_message_handler(Self::handle_update_channel_buffer); + Self { + buffer_id, + buffer, + client, + channel_id, + } + })) + }) + } + + async fn handle_update_channel_buffer( + this: ModelHandle, + update_channel_buffer: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let ops = update_channel_buffer + .payload + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; - this.update(&mut cx, |this, cx| { - let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx)); - buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + this.update(&mut cx, |this, cx| { + this.buffer + .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) + })?; - cx.subscribe(&buffer, Self::on_buffer_update).detach(); + Ok(()) + } - this.buffer = Some(buffer.clone()); - anyhow::Ok(buffer) + fn on_buffer_update( + &mut self, + _: ModelHandle, + event: &language::Event, + _: &mut ModelContext, + ) { + if let language::Event::Operation(operation) = event { + let operation = language::proto::serialize_operation(operation); + self.client + .send(proto::UpdateChannelBuffer { + buffer_id: self.buffer_id, + operations: vec![operation], }) - }) + .log_err(); } } + + pub fn buffer(&self) -> ModelHandle { + self.buffer.clone() + } } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 1e4663a6f63d990e774bd6047a1b4797a9f30fc1..12ff2caec5fd10728a2e7d15b559b0d0a256121c 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -189,8 +189,7 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT now, - "main_buffer_id" INTEGER REFERENCES buffers (id) + "created_at" TIMESTAMP NOT NULL DEFAULT now ); CREATE TABLE "channel_paths" ( @@ -212,9 +211,12 @@ CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channe CREATE TABLE "buffers" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL DEFAULT 0 ); +CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); + CREATE TABLE "buffer_operations" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, @@ -233,3 +235,18 @@ CREATE TABLE "buffer_snapshots" ( "text" TEXT NOT NULL, PRIMARY KEY(buffer_id, epoch) ); + +CREATE TABLE "channel_buffer_collaborators" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "replica_id" INTEGER NOT NULL +); + +CREATE INDEX "index_channel_buffer_collaborators_on_buffer_id" ON "channel_buffer_collaborators" ("buffer_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_and_replica_id" ON "channel_buffer_collaborators" ("buffer_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("buffer_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql index a4d936fd74bdd674b115109cb4640a4159699b4b..8ccd7acadf39e77e39ad41f6b8bc3f9fe9f38713 100644 --- a/crates/collab/migrations/20230819154600_add_channel_buffers.sql +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -1,8 +1,11 @@ CREATE TABLE "buffers" ( "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL DEFAULT 0 ); +CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); + CREATE TABLE "buffer_operations" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, @@ -22,4 +25,17 @@ CREATE TABLE "buffer_snapshots" ( PRIMARY KEY(buffer_id, epoch) ); -ALTER TABLE "channels" ADD COLUMN "main_buffer_id" INTEGER REFERENCES buffers (id); +CREATE TABLE "channel_buffer_collaborators" ( + "id" SERIAL PRIMARY KEY, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "replica_id" INTEGER NOT NULL +); + +CREATE INDEX "index_channel_buffer_collaborators_on_buffer_id" ON "channel_buffer_collaborators" ("buffer_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_and_replica_id" ON "channel_buffer_collaborators" ("buffer_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("buffer_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 54f9463ccac62aab39fb1e25afd68a9a2763e98c..8501083f839940ed9723813b9aac8a029d706a0d 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -124,3 +124,4 @@ id_type!(ReplicaId); id_type!(ServerId); id_type!(SignupId); id_type!(UserId); +id_type!(ChannelBufferCollaboratorId); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index f5ff2e33679558954546a842be91439f507b62eb..ba88e95fb8cf15e7c3b17e4826435896287f3e18 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,20 +1,12 @@ use super::*; use prost::Message; -pub struct Buffer { +pub struct ChannelBuffer { pub base_text: String, pub operations: Vec, } impl Database { - pub async fn create_buffer(&self) -> Result { - self.transaction(|tx| async move { - let buffer = buffer::ActiveModel::new().insert(&*tx).await?; - Ok(buffer.id) - }) - .await - } - pub async fn update_buffer( &self, buffer_id: BufferId, @@ -69,13 +61,65 @@ impl Database { .await } - pub async fn get_buffer(&self, id: BufferId) -> Result { + pub async fn join_buffer_for_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + connection: ConnectionId, + ) -> Result { self.transaction(|tx| async move { - let buffer = buffer::Entity::find_by_id(id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such buffer"))?; + let tx = tx; + // Get or create buffer from channel + self.check_user_is_channel_member(channel_id, user_id, &tx) + .await?; + + let buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await?; + + let buffer = if let Some(buffer) = buffer { + buffer + } else { + let buffer = buffer::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + ..Default::default() + } + .insert(&*tx) + .await?; + buffer + }; + + // Join the collaborators + let collaborators = buffer + .find_related(channel_buffer_collaborator::Entity) + .all(&*tx) + .await?; + let replica_ids = collaborators + .iter() + .map(|c| c.replica_id) + .collect::>(); + let mut replica_id = ReplicaId(0); + while replica_ids.contains(&replica_id) { + replica_id.0 += 1; + } + channel_buffer_collaborator::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + connection_id: ActiveValue::Set(connection.id as i32), + connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::Set(user_id), + replica_id: ActiveValue::Set(replica_id), + ..Default::default() + } + .insert(&*tx) + .await?; + + // Assemble the buffer state + let id = buffer.id; let base_text = if buffer.epoch > 0 { buffer_snapshot::Entity::find() .filter( @@ -128,13 +172,44 @@ impl Database { }) } - Ok(Buffer { + Ok(ChannelBuffer { base_text, operations, }) }) .await } + + pub async fn get_buffer_collaborators(&self, buffer: BufferId) -> Result<()> { + todo!() + } + + pub async fn leave_buffer(&self, buffer: BufferId, user: UserId) -> Result<()> { + self.transaction(|tx| async move { + //TODO + // let tx = tx; + // let channel = channel::Entity::find_by_id(channel_id) + // .one(&*tx) + // .await? + // .ok_or_else(|| anyhow!("invalid channel"))?; + + // if let Some(id) = channel.main_buffer_id { + // return Ok(id); + // } else { + // let buffer = buffer::ActiveModel::new().insert(&*tx).await?; + // channel::ActiveModel { + // id: ActiveValue::Unchanged(channel_id), + // main_buffer_id: ActiveValue::Set(Some(buffer.id)), + // ..Default::default() + // } + // .update(&*tx) + // .await?; + // Ok(buffer.id) + // } + Ok(()) + }) + .await + } } mod storage { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 85a9304a2ec2cf7062f0a0b968692bce24d9f438..e3d3643a61f3b1fb1b498db9ef6ae36d1a1aa5ce 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -689,34 +689,6 @@ impl Database { }) .await } - - pub async fn get_or_create_buffer_for_channel( - &self, - channel_id: ChannelId, - ) -> Result { - self.transaction(|tx| async move { - let tx = tx; - let channel = channel::Entity::find_by_id(channel_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid channel"))?; - - if let Some(id) = channel.main_buffer_id { - return Ok(id); - } else { - let buffer = buffer::ActiveModel::new().insert(&*tx).await?; - channel::ActiveModel { - id: ActiveValue::Unchanged(channel_id), - main_buffer_id: ActiveValue::Set(Some(buffer.id)), - ..Default::default() - } - .update(&*tx) - .await?; - Ok(buffer.id) - } - }) - .await - } } #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index fbf4bff2a6efea0e467ebd7f7ff527fc18f1144b..fe747e0d27ec1cc5b67b0bbdb55a1c5992fa27b4 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -3,6 +3,7 @@ pub mod buffer; pub mod buffer_operation; pub mod buffer_snapshot; pub mod channel; +pub mod channel_buffer_collaborator; pub mod channel_member; pub mod channel_path; pub mod contact; diff --git a/crates/collab/src/db/tables/buffer.rs b/crates/collab/src/db/tables/buffer.rs index 84e62cc0712716dd6a6c2c58f152e26a9553fe69..f0187ad2787e22826ee2ed384907edd4b8a2f027 100644 --- a/crates/collab/src/db/tables/buffer.rs +++ b/crates/collab/src/db/tables/buffer.rs @@ -1,4 +1,4 @@ -use crate::db::BufferId; +use crate::db::{BufferId, ChannelId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] @@ -7,6 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: BufferId, pub epoch: i32, + pub channel_id: ChannelId, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -15,6 +16,14 @@ pub enum Relation { Operations, #[sea_orm(has_many = "super::buffer_snapshot::Entity")] Snapshots, + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, + #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] + Collaborators, } impl Related for Entity { @@ -29,4 +38,16 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Collaborators.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 444d5fa6d9230faa8fc256cf38fafe55d126c6ce..7b33e3a1dd59fbf48bd738ba40f492ae0ed19cc1 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -7,7 +7,6 @@ pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, pub name: String, - pub main_buffer_id: Option, } impl ActiveModelBehavior for ActiveModel {} @@ -16,6 +15,8 @@ impl ActiveModelBehavior for ActiveModel {} pub enum Relation { #[sea_orm(has_one = "super::room::Entity")] Room, + #[sea_orm(has_one = "super::room::Entity")] + Buffer, #[sea_orm(has_many = "super::channel_member::Entity")] Member, } @@ -31,3 +32,9 @@ impl Related for Entity { Relation::Room.def() } } + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} diff --git a/crates/collab/src/db/tables/channel_buffer_collaborator.rs b/crates/collab/src/db/tables/channel_buffer_collaborator.rs new file mode 100644 index 0000000000000000000000000000000000000000..2e43e93e8ef50db7ec29c42201ae52044d7a4bbd --- /dev/null +++ b/crates/collab/src/db/tables/channel_buffer_collaborator.rs @@ -0,0 +1,42 @@ +use crate::db::{BufferId, ChannelBufferCollaboratorId, ReplicaId, ServerId, UserId}; +use rpc::ConnectionId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_buffer_collaborators")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: ChannelBufferCollaboratorId, + pub buffer_id: BufferId, + pub connection_id: i32, + pub connection_server_id: ServerId, + pub user_id: UserId, + pub replica_id: ReplicaId, +} + +impl Model { + pub fn connection(&self) -> ConnectionId { + ConnectionId { + owner_id: self.connection_server_id.0 as u32, + id: self.connection_id as u32, + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index f0e78e1fe4a5c96b8aa5c55e6d4c400b255a07b7..bf7d7763e2f1ee4fc6d22cdd1cb7712285b2b5e3 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -6,7 +6,60 @@ use text::Buffer; test_both_dbs!(test_buffers, test_buffers_postgres, test_buffers_sqlite); async fn test_buffers(db: &Arc) { - let buffer_id = db.create_buffer().await.unwrap(); + // Prep database test info + let a_id = db + .create_user( + "user_a@example.com", + false, + NewUserParams { + github_login: "user_a".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let b_id = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + // This user will not be a part of the channel + let c_id = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + db.invite_channel_member(zed_id, b_id, a_id, false) + .await + .unwrap(); + + db.respond_to_channel_invite(zed_id, b_id, true) + .await + .unwrap(); + + // TODO: Join buffer + let buffer_id = db.get_or_create_buffer_for_channel(zed_id); let mut buffer = Buffer::new(0, 0, "".to_string()); let mut operations = Vec::new(); @@ -23,7 +76,7 @@ async fn test_buffers(db: &Arc) { db.update_buffer(buffer_id, &operations).await.unwrap(); - let buffer_data = db.get_buffer(buffer_id).await.unwrap(); + let buffer_data = db.open_buffer(buffer_id).await.unwrap(); let mut buffer_2 = Buffer::new(0, 0, buffer_data.base_text); buffer_2 diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 22eb23ce8e1ef9684a06f6d608ef1926d78abe8f..6e62b90473f11f31543ac64313a6c7e2375af6a6 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,10 @@ mod connection_pool; use crate::{ auth, - db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId}, + db::{ + self, BufferId, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, + UserId, + }, executor::Executor, AppState, Result, }; @@ -35,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, GetChannelBufferResponse, - LiveKitConnectionInfo, RequestMessage, + self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, + OpenChannelBufferResponse, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -248,7 +251,9 @@ impl Server { .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) .add_request_handler(rename_channel) - .add_request_handler(get_channel_buffer) + .add_request_handler(open_channel_buffer) + .add_request_handler(close_channel_buffer) + .add_message_handler(update_channel_buffer) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -2479,9 +2484,9 @@ async fn join_channel( Ok(()) } -async fn get_channel_buffer( - request: proto::GetChannelBuffer, - response: Response, +async fn open_channel_buffer( + request: proto::OpenChannelBuffer, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; @@ -2489,9 +2494,12 @@ async fn get_channel_buffer( let buffer_id = db.get_or_create_buffer_for_channel(channel_id).await?; - let buffer = db.get_buffer(buffer_id).await?; + // TODO: join channel_buffer + + let buffer = db.open_buffer(buffer_id).await?; - response.send(GetChannelBufferResponse { + response.send(OpenChannelBufferResponse { + buffer_id: buffer_id.to_proto(), base_text: buffer.base_text, operations: buffer.operations, })?; @@ -2499,6 +2507,32 @@ async fn get_channel_buffer( Ok(()) } +async fn close_channel_buffer( + request: proto::CloseChannelBuffer, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let buffer_id = BufferId::from_proto(request.buffer_id); + + // TODO: close channel buffer here + // + response.send(Ack {})?; + + Ok(()) +} + +async fn update_channel_buffer( + request: proto::UpdateChannelBuffer, + session: Session, +) -> Result<()> { + let db = session.db().await; + + // TODO: Broadcast to buffer members + + Ok(()) +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index e7f662523e5aae23b4e320bf7e28f0c1eb3146b6..c41f5de803691d69e70a2c3fb9847e15ed06d732 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -19,45 +19,39 @@ async fn test_channel_buffers( .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; - let a_document = - cx_a.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx)); - let channel_buffer_a = a_document - .update(cx_a, |doc, cx| doc.buffer(cx)) + let channel_buffer_a = cx_a + .update(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx)) .await .unwrap(); - edit_channel_buffer(&channel_buffer_a, cx_a, [(0..0, "hello world")]); - edit_channel_buffer(&channel_buffer_a, cx_a, [(5..5, ", cruel")]); - edit_channel_buffer(&channel_buffer_a, cx_a, [(0..5, "goodbye")]); - undo_channel_buffer(&channel_buffer_a, cx_a); + let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); - assert_eq!( - channel_buffer_text(&channel_buffer_a, cx_a), - "hello, cruel world" - ); + edit_channel_buffer(&buffer_a, cx_a, [(0..0, "hello world")]); + edit_channel_buffer(&buffer_a, cx_a, [(5..5, ", cruel")]); + edit_channel_buffer(&buffer_a, cx_a, [(0..5, "goodbye")]); + undo_channel_buffer(&buffer_a, cx_a); + + assert_eq!(channel_buffer_text(&buffer_a, cx_a), "hello, cruel world"); - let b_document = - cx_b.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx)); - let channel_buffer_b = b_document - .update(cx_b, |doc, cx| doc.buffer(cx)) + let channel_buffer_b = cx_b + .update(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx)) .await .unwrap(); - assert_eq!( - channel_buffer_text(&channel_buffer_b, cx_b), - "hello, cruel world" - ); + let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + + assert_eq!(channel_buffer_text(&buffer_b, cx_b), "hello, cruel world"); - edit_channel_buffer(&channel_buffer_b, cx_b, [(7..12, "beautiful")]); + edit_channel_buffer(&buffer_b, cx_b, [(7..12, "beautiful")]); deterministic.run_until_parked(); assert_eq!( - channel_buffer_text(&channel_buffer_a, cx_a), + channel_buffer_text(&buffer_a, cx_a), "hello, beautiful world" ); assert_eq!( - channel_buffer_text(&channel_buffer_b, cx_b), + channel_buffer_text(&buffer_b, cx_b), "hello, beautiful world" ); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index baeaae187622a0ca7ea5c4c43b8fd5f07cd09a46..7fb22577f31d6d5dea4f1274413866643104412f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -143,8 +143,10 @@ message Envelope { SetChannelMemberAdmin set_channel_member_admin = 129; RenameChannel rename_channel = 130; - GetChannelBuffer get_channel_buffer = 131; - GetChannelBufferResponse get_channel_buffer_response = 132; + OpenChannelBuffer open_channel_buffer = 131; + OpenChannelBufferResponse open_channel_buffer_response = 132; + UpdateChannelBuffer update_channel_buffer = 133; + CloseChannelBuffer close_channel_buffer = 134; } } @@ -543,6 +545,11 @@ message UpdateBuffer { repeated Operation operations = 3; } +message UpdateChannelBuffer { + uint64 buffer_id = 2; + repeated Operation operations = 3; +} + message UpdateBufferFile { uint64 project_id = 1; uint64 buffer_id = 2; @@ -951,13 +958,18 @@ message RenameChannel { string name = 2; } -message GetChannelBuffer { +message OpenChannelBuffer { uint64 channel_id = 1; } -message GetChannelBufferResponse { - string base_text = 1; - repeated Operation operations = 2; +message OpenChannelBufferResponse { + uint64 buffer_id = 1; + string base_text = 2; + repeated Operation operations = 3; +} + +message CloseChannelBuffer { + uint64 buffer_id = 1; } message RespondToChannelInvite { @@ -1156,7 +1168,6 @@ enum GitStatus { Conflict = 2; } - message BufferState { uint64 id = 1; optional File file = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 21a491b9342ce40b158333d96a4d98e20418fc70..9d71140aa0795f6a76617fec370d7656150613f7 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -249,8 +249,10 @@ messages!( (GetPrivateUserInfoResponse, Foreground), (GetChannelMembers, Foreground), (GetChannelMembersResponse, Foreground), - (GetChannelBuffer, Foreground), - (GetChannelBufferResponse, Foreground) + (OpenChannelBuffer, Foreground), + (OpenChannelBufferResponse, Foreground), + (CloseChannelBuffer, Background), + (UpdateChannelBuffer, Foreground) ); request_messages!( @@ -317,7 +319,8 @@ request_messages!( (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), - (GetChannelBuffer, GetChannelBufferResponse) + (OpenChannelBuffer, OpenChannelBufferResponse), + (CloseChannelBuffer, Ack) ); entity_messages!( @@ -373,6 +376,8 @@ entity_messages!( UpdateDiffBase ); +entity_messages!(buffer_id, UpdateChannelBuffer); + const KIB: usize = 1024; const MIB: usize = KIB * 1024; const MAX_BUFFER_LEN: usize = MIB; From 71611ee7a2ef52a785df74e79af2dd3ececdf706 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 09:47:49 -0700 Subject: [PATCH 026/142] Get join_buffer_for_channel compiling Co-authored-by: Mikayla --- crates/collab/src/db/queries/buffers.rs | 23 +++++--- crates/collab/src/db/tables/channel.rs | 4 +- crates/collab/src/db/tests/buffer_tests.rs | 67 +++++++++++++++++----- crates/collab/src/db/tests/db_tests.rs | 29 ---------- crates/collab/src/rpc.rs | 14 ++--- crates/rpc/proto/zed.proto | 1 + 6 files changed, 73 insertions(+), 65 deletions(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index ba88e95fb8cf15e7c3b17e4826435896287f3e18..3f86f897d88875f1945de22b46dcc44c066ac323 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,11 +1,6 @@ use super::*; use prost::Message; -pub struct ChannelBuffer { - pub base_text: String, - pub operations: Vec, -} - impl Database { pub async fn update_buffer( &self, @@ -66,7 +61,7 @@ impl Database { channel_id: ChannelId, user_id: UserId, connection: ConnectionId, - ) -> Result { + ) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -95,7 +90,7 @@ impl Database { }; // Join the collaborators - let collaborators = buffer + let mut collaborators = buffer .find_related(channel_buffer_collaborator::Entity) .all(&*tx) .await?; @@ -107,7 +102,7 @@ impl Database { while replica_ids.contains(&replica_id) { replica_id.0 += 1; } - channel_buffer_collaborator::ActiveModel { + let collaborator = channel_buffer_collaborator::ActiveModel { buffer_id: ActiveValue::Set(buffer.id), connection_id: ActiveValue::Set(connection.id as i32), connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), @@ -117,6 +112,7 @@ impl Database { } .insert(&*tx) .await?; + collaborators.push(collaborator); // Assemble the buffer state let id = buffer.id; @@ -172,9 +168,18 @@ impl Database { }) } - Ok(ChannelBuffer { + Ok(proto::OpenChannelBufferResponse { + buffer_id: buffer.id.to_proto(), base_text, operations, + collaborators: collaborators + .into_iter() + .map(|collaborator| proto::Collaborator { + peer_id: Some(collaborator.connection().into()), + user_id: collaborator.user_id.to_proto(), + replica_id: collaborator.replica_id.0 as u32, + }) + .collect(), }) }) .await diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 7b33e3a1dd59fbf48bd738ba40f492ae0ed19cc1..7f59e8d65f73e8c44a2bdf4a261da1a597546f6e 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -1,4 +1,4 @@ -use crate::db::{BufferId, ChannelId}; +use crate::db::ChannelId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -15,7 +15,7 @@ impl ActiveModelBehavior for ActiveModel {} pub enum Relation { #[sea_orm(has_one = "super::room::Entity")] Room, - #[sea_orm(has_one = "super::room::Entity")] + #[sea_orm(has_one = "super::buffer::Entity")] Buffer, #[sea_orm(has_many = "super::channel_member::Entity")] Member, diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index bf7d7763e2f1ee4fc6d22cdd1cb7712285b2b5e3..fff9938573fc81c550b39f762a69624defaf21d3 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -3,9 +3,13 @@ use crate::test_both_dbs; use language::proto; use text::Buffer; -test_both_dbs!(test_buffers, test_buffers_postgres, test_buffers_sqlite); +test_both_dbs!( + test_channel_buffers, + test_channel_buffers_postgres, + test_channel_buffers_sqlite +); -async fn test_buffers(db: &Arc) { +async fn test_channel_buffers(db: &Arc) { // Prep database test info let a_id = db .create_user( @@ -48,6 +52,8 @@ async fn test_buffers(db: &Arc) { .unwrap() .user_id; + let owner_id = db.create_server("production").await.unwrap().0 as u32; + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); db.invite_channel_member(zed_id, b_id, a_id, false) @@ -58,16 +64,19 @@ async fn test_buffers(db: &Arc) { .await .unwrap(); - // TODO: Join buffer - let buffer_id = db.get_or_create_buffer_for_channel(zed_id); + let buffer_response_a = db + .join_buffer_for_channel(zed_id, a_id, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + let buffer_id = BufferId::from_proto(buffer_response_a.buffer_id); - let mut buffer = Buffer::new(0, 0, "".to_string()); + let mut buffer_a = Buffer::new(0, 0, "".to_string()); let mut operations = Vec::new(); - operations.push(buffer.edit([(0..0, "hello world")])); - operations.push(buffer.edit([(5..5, ", cruel")])); - operations.push(buffer.edit([(0..5, "goodbye")])); - operations.push(buffer.undo().unwrap().1); - assert_eq!(buffer.text(), "hello, cruel world"); + operations.push(buffer_a.edit([(0..0, "hello world")])); + operations.push(buffer_a.edit([(5..5, ", cruel")])); + operations.push(buffer_a.edit([(0..5, "goodbye")])); + operations.push(buffer_a.undo().unwrap().1); + assert_eq!(buffer_a.text(), "hello, cruel world"); let operations = operations .into_iter() @@ -76,11 +85,14 @@ async fn test_buffers(db: &Arc) { db.update_buffer(buffer_id, &operations).await.unwrap(); - let buffer_data = db.open_buffer(buffer_id).await.unwrap(); + let buffer_response_b = db + .join_buffer_for_channel(zed_id, b_id, ConnectionId { owner_id, id: 2 }) + .await + .unwrap(); - let mut buffer_2 = Buffer::new(0, 0, buffer_data.base_text); - buffer_2 - .apply_ops(buffer_data.operations.into_iter().map(|operation| { + let mut buffer_b = Buffer::new(0, 0, buffer_response_b.base_text); + buffer_b + .apply_ops(buffer_response_b.operations.into_iter().map(|operation| { let operation = proto::deserialize_operation(operation).unwrap(); if let language::Operation::Buffer(operation) = operation { operation @@ -90,5 +102,30 @@ async fn test_buffers(db: &Arc) { })) .unwrap(); - assert_eq!(buffer_2.text(), "hello, cruel world"); + assert_eq!(buffer_b.text(), "hello, cruel world"); + + // Ensure that C fails to open the buffer + assert!(db + .join_buffer_for_channel(zed_id, c_id, ConnectionId { owner_id, id: 3 }) + .await + .is_err()); + + //Ensure that both collaborators have shown up + assert_eq!( + buffer_response_b.collaborators, + &[ + rpc::proto::Collaborator { + user_id: a_id.to_proto(), + peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), + replica_id: 0, + }, + rpc::proto::Collaborator { + user_id: b_id.to_proto(), + peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), + replica_id: 1, + } + ] + ); + + // Leave buffer } diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 0fffabc7c43a3a77bc5e2837fc66973a78a2c028..fc31ee7c4d4aee8dddc46bf6cc0e77fc89e4dd39 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -1329,35 +1329,6 @@ async fn test_channel_renames(db: &Arc) { assert!(bad_name_rename.is_err()) } -test_both_dbs!( - test_get_or_create_channel_buffer, - test_get_or_create_channel_buffer_postgres, - test_get_or_create_channel_buffer_sqlite -); - -async fn test_get_or_create_channel_buffer(db: &Arc) { - let a_id = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); - - let first_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap(); - let second_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap(); - - assert_eq!(first_buffer_id, second_buffer_id); -} - #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6e62b90473f11f31543ac64313a6c7e2375af6a6..95c6bdefc1672fbf3b7bc9e99a113b0391c7035a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2492,17 +2492,11 @@ async fn open_channel_buffer( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let buffer_id = db.get_or_create_buffer_for_channel(channel_id).await?; - - // TODO: join channel_buffer - - let buffer = db.open_buffer(buffer_id).await?; + let open_response = db + .join_buffer_for_channel(channel_id, session.user_id, session.connection_id) + .await?; - response.send(OpenChannelBufferResponse { - buffer_id: buffer_id.to_proto(), - base_text: buffer.base_text, - operations: buffer.operations, - })?; + response.send(open_response)?; Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 7fb22577f31d6d5dea4f1274413866643104412f..6f19132dc5576c6bff09255c70c731d2c797ddac 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -966,6 +966,7 @@ message OpenChannelBufferResponse { uint64 buffer_id = 1; string base_text = 2; repeated Operation operations = 3; + repeated Collaborator collaborators = 4; } message CloseChannelBuffer { From 95ea6647259d812da38c5b473600dc22ec602a8d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 11:02:13 -0700 Subject: [PATCH 027/142] WIP --- crates/channel/src/channel_buffer.rs | 2 +- .../20221109000000_test_schema.sql | 9 +- .../20230819154600_add_channel_buffers.sql | 9 +- crates/collab/src/db/queries/buffers.rs | 188 ++++++++++-------- crates/collab/src/db/queries/rooms.rs | 36 +++- crates/collab/src/db/tables/buffer.rs | 8 - crates/collab/src/db/tables/channel.rs | 8 + .../db/tables/channel_buffer_collaborator.rs | 17 +- crates/collab/src/db/tests/buffer_tests.rs | 28 ++- crates/collab/src/rpc.rs | 28 +-- crates/rpc/proto/zed.proto | 14 +- crates/rpc/src/proto.rs | 10 +- 12 files changed, 211 insertions(+), 146 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 372bd319a135b47762db2cbac4d2c54f5aad0dd1..d88810ff56f01ab96c224ac86068de6f800ad50f 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -36,7 +36,7 @@ impl ChannelBuffer { ) -> Task>> { cx.spawn(|mut cx| async move { let response = client - .request(proto::OpenChannelBuffer { channel_id }) + .request(proto::JoinChannelBuffer { channel_id }) .await?; let base_text = response.base_text; diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 12ff2caec5fd10728a2e7d15b559b0d0a256121c..f39f0cca5909f28e11b91b57d73fc8d0cd5c8a47 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -238,15 +238,16 @@ CREATE TABLE "buffer_snapshots" ( CREATE TABLE "channel_buffer_collaborators" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "connection_id" INTEGER NOT NULL, "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "connection_lost" BOOLEAN NOT NULL DEFAULT false, "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "replica_id" INTEGER NOT NULL ); -CREATE INDEX "index_channel_buffer_collaborators_on_buffer_id" ON "channel_buffer_collaborators" ("buffer_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_and_replica_id" ON "channel_buffer_collaborators" ("buffer_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("buffer_id", "connection_id", "connection_server_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql index 8ccd7acadf39e77e39ad41f6b8bc3f9fe9f38713..f6bd2879c618ac2851a01b545f1f8eb8ec7c746d 100644 --- a/crates/collab/migrations/20230819154600_add_channel_buffers.sql +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -27,15 +27,16 @@ CREATE TABLE "buffer_snapshots" ( CREATE TABLE "channel_buffer_collaborators" ( "id" SERIAL PRIMARY KEY, - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "connection_id" INTEGER NOT NULL, "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "connection_lost" BOOLEAN NOT NULL DEFAULT FALSE, "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "replica_id" INTEGER NOT NULL ); -CREATE INDEX "index_channel_buffer_collaborators_on_buffer_id" ON "channel_buffer_collaborators" ("buffer_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_and_replica_id" ON "channel_buffer_collaborators" ("buffer_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("buffer_id", "connection_id", "connection_server_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 3f86f897d88875f1945de22b46dcc44c066ac323..473dd1afe98b6ed433b744bcf911da4f8c932905 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -2,66 +2,12 @@ use super::*; use prost::Message; impl Database { - pub async fn update_buffer( - &self, - buffer_id: BufferId, - operations: &[proto::Operation], - ) -> Result<()> { - self.transaction(|tx| async move { - let buffer = buffer::Entity::find_by_id(buffer_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such buffer"))?; - buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { - match operation.variant.as_ref()? { - proto::operation::Variant::Edit(operation) => { - let value = - serialize_edit_operation(&operation.ranges, &operation.new_text); - let version = serialize_version(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer_id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(false), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::Undo(operation) => { - let value = serialize_undo_operation(&operation.counts); - let version = serialize_version(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer_id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(true), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::UpdateSelections(_) => None, - proto::operation::Variant::UpdateDiagnostics(_) => None, - proto::operation::Variant::UpdateCompletionTriggers(_) => None, - } - })) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - pub async fn join_buffer_for_channel( + pub async fn join_channel_buffer( &self, channel_id: ChannelId, user_id: UserId, connection: ConnectionId, - ) -> Result { + ) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -90,8 +36,8 @@ impl Database { }; // Join the collaborators - let mut collaborators = buffer - .find_related(channel_buffer_collaborator::Entity) + let mut collaborators = channel_buffer_collaborator::Entity::find() + .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) .all(&*tx) .await?; let replica_ids = collaborators @@ -103,7 +49,7 @@ impl Database { replica_id.0 += 1; } let collaborator = channel_buffer_collaborator::ActiveModel { - buffer_id: ActiveValue::Set(buffer.id), + channel_id: ActiveValue::Set(channel_id), connection_id: ActiveValue::Set(connection.id as i32), connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), user_id: ActiveValue::Set(user_id), @@ -168,7 +114,7 @@ impl Database { }) } - Ok(proto::OpenChannelBufferResponse { + Ok(proto::JoinChannelBufferResponse { buffer_id: buffer.id.to_proto(), base_text, operations, @@ -185,32 +131,112 @@ impl Database { .await } - pub async fn get_buffer_collaborators(&self, buffer: BufferId) -> Result<()> { + pub async fn leave_channel_buffer( + &self, + channel_id: ChannelId, + connection: ConnectionId, + ) -> Result> { + self.transaction(|tx| async move { + let result = channel_buffer_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .add( + channel_buffer_collaborator::Column::ConnectionId + .eq(connection.id as i32), + ) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("not a collaborator on this project"))?; + } + + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + Ok(connections) + }) + .await + } + + pub async fn leave_channel_buffers( + &self, + connection: ConnectionId, + ) -> Result> { + // + } + + pub async fn get_channel_buffer_collaborators(&self, channel_id: ChannelId) -> Result<()> { todo!() } - pub async fn leave_buffer(&self, buffer: BufferId, user: UserId) -> Result<()> { + pub async fn update_channel_buffer( + &self, + buffer_id: BufferId, + operations: &[proto::Operation], + ) -> Result<()> { self.transaction(|tx| async move { - //TODO - // let tx = tx; - // let channel = channel::Entity::find_by_id(channel_id) - // .one(&*tx) - // .await? - // .ok_or_else(|| anyhow!("invalid channel"))?; + let buffer = buffer::Entity::find_by_id(buffer_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { + match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => { + let value = + serialize_edit_operation(&operation.ranges, &operation.new_text); + let version = serialize_version(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(false), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::Undo(operation) => { + let value = serialize_undo_operation(&operation.counts); + let version = serialize_version(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(true), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::UpdateSelections(_) => None, + proto::operation::Variant::UpdateDiagnostics(_) => None, + proto::operation::Variant::UpdateCompletionTriggers(_) => None, + } + })) + .exec(&*tx) + .await?; - // if let Some(id) = channel.main_buffer_id { - // return Ok(id); - // } else { - // let buffer = buffer::ActiveModel::new().insert(&*tx).await?; - // channel::ActiveModel { - // id: ActiveValue::Unchanged(channel_id), - // main_buffer_id: ActiveValue::Set(Some(buffer.id)), - // ..Default::default() - // } - // .update(&*tx) - // .await?; - // Ok(buffer.id) - // } Ok(()) }) .await diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index ee79f2cb4f9347c0a12750009d3e3628a6d99c47..a85d257187c2207b56b934540ba34c566eb1c77d 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -903,15 +903,35 @@ impl Database { ), ) .one(&*tx) - .await? - .ok_or_else(|| anyhow!("not a participant in any room"))?; + .await?; - room_participant::Entity::update(room_participant::ActiveModel { - answering_connection_lost: ActiveValue::set(true), - ..participant.into_active_model() - }) - .exec(&*tx) - .await?; + if let Some(participant) = participant { + room_participant::Entity::update(room_participant::ActiveModel { + answering_connection_lost: ActiveValue::set(true), + ..participant.into_active_model() + }) + .exec(&*tx) + .await?; + } + + channel_buffer_collaborator::Entity::update_many() + .filter( + Condition::all() + .add( + channel_buffer_collaborator::Column::ConnectionId + .eq(connection.id as i32), + ) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .set(channel_buffer_collaborator::ActiveModel { + connection_lost: ActiveValue::set(true), + ..Default::default() + }) + .exec(&*tx) + .await?; Ok(()) }) diff --git a/crates/collab/src/db/tables/buffer.rs b/crates/collab/src/db/tables/buffer.rs index f0187ad2787e22826ee2ed384907edd4b8a2f027..ec2ffd4a68d1958370be7632c943c26432a7c902 100644 --- a/crates/collab/src/db/tables/buffer.rs +++ b/crates/collab/src/db/tables/buffer.rs @@ -22,8 +22,6 @@ pub enum Relation { to = "super::channel::Column::Id" )] Channel, - #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] - Collaborators, } impl Related for Entity { @@ -44,10 +42,4 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::Collaborators.def() - } -} - impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 7f59e8d65f73e8c44a2bdf4a261da1a597546f6e..05895ede4cf6b5080889cf281a1ce3651aebd1c2 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -19,6 +19,8 @@ pub enum Relation { Buffer, #[sea_orm(has_many = "super::channel_member::Entity")] Member, + #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] + BufferCollaborators, } impl Related for Entity { @@ -38,3 +40,9 @@ impl Related for Entity { Relation::Buffer.def() } } + +impl Related for Entity { + fn to() -> RelationDef { + Relation::BufferCollaborators.def() + } +} diff --git a/crates/collab/src/db/tables/channel_buffer_collaborator.rs b/crates/collab/src/db/tables/channel_buffer_collaborator.rs index 2e43e93e8ef50db7ec29c42201ae52044d7a4bbd..ac2637b36e3129d1c4582c869db398088b9055d9 100644 --- a/crates/collab/src/db/tables/channel_buffer_collaborator.rs +++ b/crates/collab/src/db/tables/channel_buffer_collaborator.rs @@ -1,4 +1,4 @@ -use crate::db::{BufferId, ChannelBufferCollaboratorId, ReplicaId, ServerId, UserId}; +use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId}; use rpc::ConnectionId; use sea_orm::entity::prelude::*; @@ -7,9 +7,10 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: ChannelBufferCollaboratorId, - pub buffer_id: BufferId, + pub channel_id: ChannelId, pub connection_id: i32, pub connection_server_id: ServerId, + pub connection_lost: bool, pub user_id: UserId, pub replica_id: ReplicaId, } @@ -26,16 +27,16 @@ impl Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( - belongs_to = "super::buffer::Entity", - from = "Column::BufferId", - to = "super::buffer::Column::Id" + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" )] - Buffer, + Channel, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::Buffer.def() + Relation::Channel.def() } } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index fff9938573fc81c550b39f762a69624defaf21d3..c25071e1a2d8f2a3845bc88e63ef38faf57298ae 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -37,13 +37,14 @@ async fn test_channel_buffers(db: &Arc) { .await .unwrap() .user_id; + // This user will not be a part of the channel let c_id = db .create_user( - "user_b@example.com", + "user_c@example.com", false, NewUserParams { - github_login: "user_b".into(), + github_login: "user_c".into(), github_user_id: 102, invite_count: 0, }, @@ -64,8 +65,9 @@ async fn test_channel_buffers(db: &Arc) { .await .unwrap(); + let connection_id_a = ConnectionId { owner_id, id: 1 }; let buffer_response_a = db - .join_buffer_for_channel(zed_id, a_id, ConnectionId { owner_id, id: 1 }) + .join_channel_buffer(zed_id, a_id, connection_id_a) .await .unwrap(); let buffer_id = BufferId::from_proto(buffer_response_a.buffer_id); @@ -83,10 +85,13 @@ async fn test_channel_buffers(db: &Arc) { .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) .collect::>(); - db.update_buffer(buffer_id, &operations).await.unwrap(); + db.update_channel_buffer(buffer_id, &operations) + .await + .unwrap(); + let connection_id_b = ConnectionId { owner_id, id: 2 }; let buffer_response_b = db - .join_buffer_for_channel(zed_id, b_id, ConnectionId { owner_id, id: 2 }) + .join_channel_buffer(zed_id, b_id, connection_id_b) .await .unwrap(); @@ -106,7 +111,7 @@ async fn test_channel_buffers(db: &Arc) { // Ensure that C fails to open the buffer assert!(db - .join_buffer_for_channel(zed_id, c_id, ConnectionId { owner_id, id: 3 }) + .join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 }) .await .is_err()); @@ -127,5 +132,14 @@ async fn test_channel_buffers(db: &Arc) { ] ); - // Leave buffer + let collaborators = db + .leave_channel_buffer(zed_id, connection_id_b) + .await + .unwrap(); + + assert_eq!(collaborators, &[connection_id_a],); + + db.connection_lost(connection_id_a).await.unwrap(); + // assert!() + // Test buffer epoch incrementing? } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 95c6bdefc1672fbf3b7bc9e99a113b0391c7035a..da5e7e639817771aeaebc285ba81a0cdadbfcbb0 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -39,7 +39,7 @@ use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - OpenChannelBufferResponse, RequestMessage, + RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -251,8 +251,8 @@ impl Server { .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) .add_request_handler(rename_channel) - .add_request_handler(open_channel_buffer) - .add_request_handler(close_channel_buffer) + .add_request_handler(join_channel_buffer) + .add_request_handler(leave_channel_buffer) .add_message_handler(update_channel_buffer) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) @@ -2484,16 +2484,16 @@ async fn join_channel( Ok(()) } -async fn open_channel_buffer( - request: proto::OpenChannelBuffer, - response: Response, +async fn join_channel_buffer( + request: proto::JoinChannelBuffer, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let open_response = db - .join_buffer_for_channel(channel_id, session.user_id, session.connection_id) + .join_channel_buffer(channel_id, session.user_id, session.connection_id) .await?; response.send(open_response)?; @@ -2501,16 +2501,18 @@ async fn open_channel_buffer( Ok(()) } -async fn close_channel_buffer( - request: proto::CloseChannelBuffer, - response: Response, +async fn leave_channel_buffer( + request: proto::LeaveChannelBuffer, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; - let buffer_id = BufferId::from_proto(request.buffer_id); + let channel_id = ChannelId::from_proto(request.channel_id); + + let collaborators_to_notify = db + .leave_channel_buffer(channel_id, session.connection_id) + .await?; - // TODO: close channel buffer here - // response.send(Ack {})?; Ok(()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6f19132dc5576c6bff09255c70c731d2c797ddac..88ad46abc7fcc0604afa1633c718283bae234e74 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -143,10 +143,10 @@ message Envelope { SetChannelMemberAdmin set_channel_member_admin = 129; RenameChannel rename_channel = 130; - OpenChannelBuffer open_channel_buffer = 131; - OpenChannelBufferResponse open_channel_buffer_response = 132; + JoinChannelBuffer join_channel_buffer = 131; + JoinChannelBufferResponse join_channel_buffer_response = 132; UpdateChannelBuffer update_channel_buffer = 133; - CloseChannelBuffer close_channel_buffer = 134; + LeaveChannelBuffer leave_channel_buffer = 134; } } @@ -958,19 +958,19 @@ message RenameChannel { string name = 2; } -message OpenChannelBuffer { +message JoinChannelBuffer { uint64 channel_id = 1; } -message OpenChannelBufferResponse { +message JoinChannelBufferResponse { uint64 buffer_id = 1; string base_text = 2; repeated Operation operations = 3; repeated Collaborator collaborators = 4; } -message CloseChannelBuffer { - uint64 buffer_id = 1; +message LeaveChannelBuffer { + uint64 channel_id = 1; } message RespondToChannelInvite { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 9d71140aa0795f6a76617fec370d7656150613f7..68219d3ad89682303fea57c9b9ce807dd03e3cae 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -249,9 +249,9 @@ messages!( (GetPrivateUserInfoResponse, Foreground), (GetChannelMembers, Foreground), (GetChannelMembersResponse, Foreground), - (OpenChannelBuffer, Foreground), - (OpenChannelBufferResponse, Foreground), - (CloseChannelBuffer, Background), + (JoinChannelBuffer, Foreground), + (JoinChannelBufferResponse, Foreground), + (LeaveChannelBuffer, Background), (UpdateChannelBuffer, Foreground) ); @@ -319,8 +319,8 @@ request_messages!( (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), - (OpenChannelBuffer, OpenChannelBufferResponse), - (CloseChannelBuffer, Ack) + (JoinChannelBuffer, JoinChannelBufferResponse), + (LeaveChannelBuffer, Ack) ); entity_messages!( From 5a0315c4d5016bcc59116219dce3a1f09c687ba9 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 22 Aug 2023 13:25:31 -0700 Subject: [PATCH 028/142] Achieve end to end channel buffer synchronization co-authored-by: max --- crates/channel/src/channel.rs | 7 + crates/channel/src/channel_buffer.rs | 91 ++++++++-- crates/collab/src/db/queries/buffers.rs | 163 +++++++++++++----- crates/collab/src/db/tests/buffer_tests.rs | 28 ++- crates/collab/src/rpc.rs | 105 +++++++++-- crates/collab/src/tests.rs | 1 + .../collab/src/tests/channel_buffer_tests.rs | 117 +++++++++---- crates/rpc/proto/zed.proto | 23 ++- crates/rpc/src/proto.rs | 11 +- crates/zed/src/main.rs | 1 + 10 files changed, 422 insertions(+), 125 deletions(-) diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 67c560a1fcacea38cb3b966a9dc122fa2ecf6074..15631b7dd312f36126ec1e13b2413fc01e5ca8af 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -1,7 +1,14 @@ mod channel_store; pub mod channel_buffer; +use std::sync::Arc; + pub use channel_store::*; +use client::Client; #[cfg(test)] mod channel_store_tests; + +pub fn init(client: &Arc) { + channel_buffer::init(client); +} diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index d88810ff56f01ab96c224ac86068de6f800ad50f..a59fec15535ec536be1dd50ff8e1d2fef2806624 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -6,30 +6,34 @@ use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; -// Open the channel document -// ChannelDocumentView { ChannelDocument, Editor } -> On clone, clones internal ChannelDocument handle, instantiates new editor -// Produces a view which is: (ChannelDocument, Editor), ChannelDocument manages subscriptions -// ChannelDocuments -> Buffers -> Editor with that buffer - -// ChannelDocuments { -// ChannleBuffers: HashMap> -// } - -type BufferId = u64; +pub(crate) fn init(client: &Arc) { + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); + client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator); + client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator); +} pub struct ChannelBuffer { channel_id: ChannelId, - buffer_id: BufferId, + collaborators: Vec, buffer: ModelHandle, client: Arc, + _subscription: client::Subscription, } impl Entity for ChannelBuffer { type Event = (); + + fn release(&mut self, _: &mut AppContext) { + self.client + .send(proto::LeaveChannelBuffer { + channel_id: self.channel_id, + }) + .log_err(); + } } impl ChannelBuffer { - pub fn for_channel( + pub fn join_channel( channel_id: ChannelId, client: Arc, cx: &mut AppContext, @@ -45,19 +49,24 @@ impl ChannelBuffer { .into_iter() .map(language::proto::deserialize_operation) .collect::, _>>()?; - let buffer_id = response.buffer_id; - let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx)); + let collaborators = response.collaborators; + + let buffer = + cx.add_model(|cx| language::Buffer::new(response.replica_id as u16, base_text, cx)); buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + let subscription = client.subscribe_to_entity(channel_id)?; + anyhow::Ok(cx.add_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); - client.add_model_message_handler(Self::handle_update_channel_buffer); + Self { - buffer_id, buffer, client, channel_id, + collaborators, + _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), } })) }) @@ -77,6 +86,7 @@ impl ChannelBuffer { .collect::, _>>()?; this.update(&mut cx, |this, cx| { + cx.notify(); this.buffer .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) })?; @@ -84,6 +94,49 @@ impl ChannelBuffer { Ok(()) } + async fn handle_add_channel_buffer_collaborator( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let collaborator = envelope.payload.collaborator.ok_or_else(|| { + anyhow::anyhow!( + "Should have gotten a collaborator in the AddChannelBufferCollaborator message" + ) + })?; + + this.update(&mut cx, |this, cx| { + this.collaborators.push(collaborator); + cx.notify(); + }); + + Ok(()) + } + + async fn handle_remove_channel_buffer_collaborator( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.collaborators.retain(|collaborator| { + if collaborator.peer_id == message.payload.peer_id { + this.buffer.update(cx, |buffer, cx| { + buffer.remove_peer(collaborator.replica_id as u16, cx) + }); + false + } else { + true + } + }); + cx.notify(); + }); + + Ok(()) + } + fn on_buffer_update( &mut self, _: ModelHandle, @@ -94,7 +147,7 @@ impl ChannelBuffer { let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { - buffer_id: self.buffer_id, + channel_id: self.channel_id, operations: vec![operation], }) .log_err(); @@ -104,4 +157,8 @@ impl ChannelBuffer { pub fn buffer(&self) -> ModelHandle { self.buffer.clone() } + + pub fn collaborators(&self) -> &[proto::Collaborator] { + &self.collaborators + } } diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 473dd1afe98b6ed433b744bcf911da4f8c932905..7f0e5a75f06b2d297059855747545e37089b0164 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -11,7 +11,6 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - // Get or create buffer from channel self.check_user_is_channel_member(channel_id, user_id, &tx) .await?; @@ -116,6 +115,7 @@ impl Database { Ok(proto::JoinChannelBufferResponse { buffer_id: buffer.id.to_proto(), + replica_id: replica_id.to_proto() as u32, base_text, operations, collaborators: collaborators @@ -137,67 +137,128 @@ impl Database { connection: ConnectionId, ) -> Result> { self.transaction(|tx| async move { - let result = channel_buffer_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) - .add( - channel_buffer_collaborator::Column::ConnectionId - .eq(connection.id as i32), - ) - .add( - channel_buffer_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("not a collaborator on this project"))?; + self.leave_channel_buffer_internal(channel_id, connection, &*tx) + .await + }) + .await + } + + pub async fn leave_channel_buffer_internal( + &self, + channel_id: ChannelId, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result> { + let result = channel_buffer_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("not a collaborator on this project"))?; + } + + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + Ok(connections) + } + + pub async fn leave_channel_buffers( + &self, + connection: ConnectionId, + ) -> Result)>> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryChannelIds { + ChannelId, } - let mut connections = Vec::new(); - let mut rows = channel_buffer_collaborator::Entity::find() - .filter( - Condition::all() - .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), - ) - .stream(&*tx) + let channel_ids: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::ChannelId) + .filter(Condition::all().add( + channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32), + )) + .into_values::<_, QueryChannelIds>() + .all(&*tx) .await?; - while let Some(row) = rows.next().await { - let row = row?; - connections.push(ConnectionId { - id: row.connection_id as u32, - owner_id: row.connection_server_id.0 as u32, - }); + + let mut result = Vec::new(); + for channel_id in channel_ids { + let collaborators = self + .leave_channel_buffer_internal(channel_id, connection, &*tx) + .await?; + result.push((channel_id, collaborators)); } - Ok(connections) + Ok(result) }) .await } - pub async fn leave_channel_buffers( + #[cfg(debug_assertions)] + pub async fn get_channel_buffer_collaborators( &self, - connection: ConnectionId, - ) -> Result> { - // - } + channel_id: ChannelId, + ) -> Result> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryUserIds { + UserId, + } - pub async fn get_channel_buffer_collaborators(&self, channel_id: ChannelId) -> Result<()> { - todo!() + let users: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::UserId) + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + Ok(users) + }) + .await } pub async fn update_channel_buffer( &self, - buffer_id: BufferId, + channel_id: ChannelId, + user: UserId, operations: &[proto::Operation], - ) -> Result<()> { + ) -> Result> { self.transaction(|tx| async move { - let buffer = buffer::Entity::find_by_id(buffer_id) + self.check_user_is_channel_member(channel_id, user, &*tx) + .await?; + + let buffer = buffer::Entity::find() + .filter(buffer::Column::ChannelId.eq(channel_id)) .one(&*tx) .await? .ok_or_else(|| anyhow!("no such buffer"))?; + let buffer_id = buffer.id; buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { match operation.variant.as_ref()? { proto::operation::Variant::Edit(operation) => { @@ -237,7 +298,23 @@ impl Database { .exec(&*tx) .await?; - Ok(()) + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + Ok(connections) }) .await } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index c25071e1a2d8f2a3845bc88e63ef38faf57298ae..08252e382eef8dc7166275acfad398e7ffeb121a 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -66,11 +66,10 @@ async fn test_channel_buffers(db: &Arc) { .unwrap(); let connection_id_a = ConnectionId { owner_id, id: 1 }; - let buffer_response_a = db + let _ = db .join_channel_buffer(zed_id, a_id, connection_id_a) .await .unwrap(); - let buffer_id = BufferId::from_proto(buffer_response_a.buffer_id); let mut buffer_a = Buffer::new(0, 0, "".to_string()); let mut operations = Vec::new(); @@ -85,7 +84,7 @@ async fn test_channel_buffers(db: &Arc) { .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) .collect::>(); - db.update_channel_buffer(buffer_id, &operations) + db.update_channel_buffer(zed_id, a_id, &operations) .await .unwrap(); @@ -115,7 +114,7 @@ async fn test_channel_buffers(db: &Arc) { .await .is_err()); - //Ensure that both collaborators have shown up + // Ensure that both collaborators have shown up assert_eq!( buffer_response_b.collaborators, &[ @@ -132,6 +131,10 @@ async fn test_channel_buffers(db: &Arc) { ] ); + // Ensure that get_channel_buffer_collaborators works + let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); + assert_eq!(zed_collaborats, &[a_id, b_id]); + let collaborators = db .leave_channel_buffer(zed_id, connection_id_b) .await @@ -139,7 +142,18 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(collaborators, &[connection_id_a],); - db.connection_lost(connection_id_a).await.unwrap(); - // assert!() - // Test buffer epoch incrementing? + let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); + let _ = db + .join_channel_buffer(cargo_id, a_id, connection_id_a) + .await + .unwrap(); + + db.leave_channel_buffers(connection_id_a).await.unwrap(); + + let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); + let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap(); + assert_eq!(zed_collaborators, &[]); + assert_eq!(cargo_collaborators, &[]); + + // TODO: test buffer epoch incrementing } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index da5e7e639817771aeaebc285ba81a0cdadbfcbb0..2bd39c861dde899f302531db2f54058dd0b048bc 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,10 +2,7 @@ mod connection_pool; use crate::{ auth, - db::{ - self, BufferId, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, - UserId, - }, + db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId}, executor::Executor, AppState, Result, }; @@ -38,8 +35,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, + self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, + LiveKitConnectionInfo, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -860,6 +857,7 @@ async fn connection_lost( futures::select_biased! { _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { leave_room_for_session(&session).await.trace_err(); + leave_channel_buffers_for_session(&session).await.trace_err(); if !session .connection_pool() @@ -872,6 +870,8 @@ async fn connection_lost( } } update_user_contacts(session.user_id, &session).await?; + + } _ = teardown.changed().fuse() => {} } @@ -2496,8 +2496,51 @@ async fn join_channel_buffer( .join_channel_buffer(channel_id, session.user_id, session.connection_id) .await?; + let replica_id = open_response.replica_id; + let collaborators = open_response.collaborators.clone(); + response.send(open_response)?; + let update = AddChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + collaborator: Some(proto::Collaborator { + user_id: session.user_id.to_proto(), + peer_id: Some(session.connection_id.into()), + replica_id, + }), + }; + channel_buffer_updated( + session.connection_id, + collaborators + .iter() + .filter_map(|collaborator| Some(collaborator.peer_id?.into())), + &update, + &session.peer, + ); + + Ok(()) +} + +async fn update_channel_buffer( + request: proto::UpdateChannelBuffer, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let collaborators = db + .update_channel_buffer(channel_id, session.user_id, &request.operations) + .await?; + + channel_buffer_updated( + session.connection_id, + collaborators, + &proto::UpdateChannelBuffer { + channel_id: channel_id.to_proto(), + operations: request.operations, + }, + &session.peer, + ); Ok(()) } @@ -2515,18 +2558,28 @@ async fn leave_channel_buffer( response.send(Ack {})?; + channel_buffer_updated( + session.connection_id, + collaborators_to_notify, + &proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + Ok(()) } -async fn update_channel_buffer( - request: proto::UpdateChannelBuffer, - session: Session, -) -> Result<()> { - let db = session.db().await; - - // TODO: Broadcast to buffer members - - Ok(()) +fn channel_buffer_updated( + sender_id: ConnectionId, + collaborators: impl IntoIterator, + message: &T, + peer: &Peer, +) { + broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| { + peer.send(peer_id.into(), message.clone()) + }); } async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { @@ -2854,6 +2907,28 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { Ok(()) } +async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { + let left_channel_buffers = session + .db() + .await + .leave_channel_buffers(session.connection_id) + .await?; + + for (channel_id, connections) in left_channel_buffers { + channel_buffer_updated( + session.connection_id, + connections, + &proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + } + + Ok(()) +} + fn project_left(project: &db::LeftProject, session: &Session) { for connection_id in &project.connection_ids { if project.host_user_id == session.user_id { diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 831bccbb724ec02bd0ab28cea742948902c39180..25f059c0aa959fe20116dd7596682adf6a02f945 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -211,6 +211,7 @@ impl TestServer { workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); + channel::init(&client); }); client diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index c41f5de803691d69e70a2c3fb9847e15ed06d732..d9880496f63b5060a9bf249bfe487e2ff5a49a51 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,11 +1,13 @@ -use crate::tests::TestServer; +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use channel::channel_buffer::ChannelBuffer; +use client::UserId; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; -use std::{ops::Range, sync::Arc}; +use rpc::{proto, RECEIVE_TIMEOUT}; +use std::sync::Arc; #[gpui::test] -async fn test_channel_buffers( +async fn test_core_channel_buffers( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -19,60 +21,103 @@ async fn test_channel_buffers( .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; + // Client A joins the channel buffer let channel_buffer_a = cx_a - .update(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx)) + .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) .await .unwrap(); + // Client A edits the buffer let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); - edit_channel_buffer(&buffer_a, cx_a, [(0..0, "hello world")]); - edit_channel_buffer(&buffer_a, cx_a, [(5..5, ", cruel")]); - edit_channel_buffer(&buffer_a, cx_a, [(0..5, "goodbye")]); - undo_channel_buffer(&buffer_a, cx_a); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..0, "hello world")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(5..5, ", cruel")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..5, "goodbye")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); + deterministic.run_until_parked(); - assert_eq!(channel_buffer_text(&buffer_a, cx_a), "hello, cruel world"); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); + // Client B joins the channel buffer let channel_buffer_b = cx_b - .update(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx)) + .update(|cx| ChannelBuffer::join_channel(zed_id, client_b.client().to_owned(), cx)) .await .unwrap(); - let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); - assert_eq!(channel_buffer_text(&buffer_b, cx_b), "hello, cruel world"); + // Client B sees the correct text, and then edits it + let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); + buffer_b.update(cx_b, |buffer, cx| { + buffer.edit([(7..12, "beautiful")], None, cx) + }); - edit_channel_buffer(&buffer_b, cx_b, [(7..12, "beautiful")]); + // Both A and B see the new edit + deterministic.run_until_parked(); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); + // Client A closes the channel buffer. + cx_a.update(|_| drop(channel_buffer_a)); deterministic.run_until_parked(); - assert_eq!( - channel_buffer_text(&buffer_a, cx_a), - "hello, beautiful world" - ); - assert_eq!( - channel_buffer_text(&buffer_b, cx_b), - "hello, beautiful world" - ); -} + // Client B sees that client A is gone from the channel buffer. + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); -fn edit_channel_buffer( - channel_buffer: &ModelHandle, - cx: &mut TestAppContext, - edits: I, -) where - I: IntoIterator, &'static str)>, -{ - channel_buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx)); + // Client A rejoins the channel buffer + let _channel_buffer_a = cx_a + .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Sanity test, make sure we saw A rejoining + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + &buffer.collaborators(), + &[client_b.user_id(), client_a.user_id()], + ); + }); + + // Client A loses connection. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + // Client B observes A disconnect + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // TODO: + // - Test synchronizing offline updates, what happens to A's channel buffer? } -fn undo_channel_buffer(channel_buffer: &ModelHandle, cx: &mut TestAppContext) { - channel_buffer.update(cx, |buffer, cx| buffer.undo(cx)); +#[track_caller] +fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { + assert_eq!( + collaborators + .into_iter() + .map(|collaborator| collaborator.user_id) + .collect::>(), + ids.into_iter().map(|id| id.unwrap()).collect::>() + ); } -fn channel_buffer_text( - channel_buffer: &ModelHandle, - cx: &mut TestAppContext, -) -> String { +fn buffer_text(channel_buffer: &ModelHandle, cx: &mut TestAppContext) -> String { channel_buffer.read_with(cx, |buffer, _| buffer.text()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 88ad46abc7fcc0604afa1633c718283bae234e74..b97feff06ba6ab72db602cbd5b307778501c2a19 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -147,6 +147,8 @@ message Envelope { JoinChannelBufferResponse join_channel_buffer_response = 132; UpdateChannelBuffer update_channel_buffer = 133; LeaveChannelBuffer leave_channel_buffer = 134; + AddChannelBufferCollaborator add_channel_buffer_collaborator = 135; + RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; } } @@ -416,6 +418,16 @@ message RemoveProjectCollaborator { PeerId peer_id = 2; } +message AddChannelBufferCollaborator { + uint64 channel_id = 1; + Collaborator collaborator = 2; +} + +message RemoveChannelBufferCollaborator { + uint64 channel_id = 1; + PeerId peer_id = 2; +} + message GetDefinition { uint64 project_id = 1; uint64 buffer_id = 2; @@ -546,8 +558,8 @@ message UpdateBuffer { } message UpdateChannelBuffer { - uint64 buffer_id = 2; - repeated Operation operations = 3; + uint64 channel_id = 1; + repeated Operation operations = 2; } message UpdateBufferFile { @@ -964,9 +976,10 @@ message JoinChannelBuffer { message JoinChannelBufferResponse { uint64 buffer_id = 1; - string base_text = 2; - repeated Operation operations = 3; - repeated Collaborator collaborators = 4; + uint32 replica_id = 2; + string base_text = 3; + repeated Operation operations = 4; + repeated Collaborator collaborators = 5; } message LeaveChannelBuffer { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 68219d3ad89682303fea57c9b9ce807dd03e3cae..f0f49c6230c0229c2067c9c8fcd49ba9bf850795 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -252,7 +252,9 @@ messages!( (JoinChannelBuffer, Foreground), (JoinChannelBufferResponse, Foreground), (LeaveChannelBuffer, Background), - (UpdateChannelBuffer, Foreground) + (UpdateChannelBuffer, Foreground), + (RemoveChannelBufferCollaborator, Foreground), + (AddChannelBufferCollaborator, Foreground), ); request_messages!( @@ -376,7 +378,12 @@ entity_messages!( UpdateDiffBase ); -entity_messages!(buffer_id, UpdateChannelBuffer); +entity_messages!( + channel_id, + UpdateChannelBuffer, + RemoveChannelBufferCollaborator, + AddChannelBufferCollaborator +); const KIB: usize = 1024; const MIB: usize = KIB * 1024; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b905c1d37bc8bf040e78393c2f07d63c4eecc996..3b1fccb927b9397b723006a5f868cd6aa37bff50 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -158,6 +158,7 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(Assets, cx); + channel::init(&client); diagnostics::init(cx); search::init(cx); semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); From 4eff8ad18692a505f22e552422f443cfd583d012 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 22 Aug 2023 14:18:32 -0700 Subject: [PATCH 029/142] Add channel notes view co-authored-by: Max --- Cargo.lock | 1 + crates/channel/src/channel_buffer.rs | 14 +++- crates/channel/src/channel_store.rs | 10 +++ .../collab/src/tests/channel_buffer_tests.rs | 19 ++--- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/channel_view.rs | 69 +++++++++++++++++++ crates/collab_ui/src/collab_panel.rs | 46 ++++++++++++- crates/collab_ui/src/collab_ui.rs | 1 + crates/gpui/src/app.rs | 3 +- 9 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 crates/collab_ui/src/channel_view.rs diff --git a/Cargo.lock b/Cargo.lock index a40aa7d89ce879c7be11856321e0a4bb5815ef23..deed9a176ece67f0253b3babd2c7feb32115a26d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1529,6 +1529,7 @@ dependencies = [ "futures 0.3.28", "fuzzy", "gpui", + "language", "log", "menu", "picker", diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index a59fec15535ec536be1dd50ff8e1d2fef2806624..aa99d5c10b34865e98a6424bb1683699bddd9855 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,4 +1,4 @@ -use crate::ChannelId; +use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; use client::Client; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; @@ -16,6 +16,7 @@ pub struct ChannelBuffer { channel_id: ChannelId, collaborators: Vec, buffer: ModelHandle, + channel_store: ModelHandle, client: Arc, _subscription: client::Subscription, } @@ -33,7 +34,8 @@ impl Entity for ChannelBuffer { } impl ChannelBuffer { - pub fn join_channel( + pub(crate) fn new( + channel_store: ModelHandle, channel_id: ChannelId, client: Arc, cx: &mut AppContext, @@ -65,6 +67,7 @@ impl ChannelBuffer { buffer, client, channel_id, + channel_store, collaborators, _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), } @@ -161,4 +164,11 @@ impl ChannelBuffer { pub fn collaborators(&self) -> &[proto::Collaborator] { &self.collaborators } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() + } } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index b9b2c98acd3d1ca1a8547112731427e69cd2bfa7..a6aad19d03ac45439ca7ed5ab3dc6421b104b254 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -13,6 +13,8 @@ use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; +use crate::channel_buffer::ChannelBuffer; + pub type ChannelId = u64; pub struct ChannelStore { @@ -151,6 +153,14 @@ impl ChannelStore { self.channels_by_id.get(&channel_id) } + pub fn open_channel_buffer( + &self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + ChannelBuffer::new(cx.handle(), channel_id, self.client.clone(), cx) + } + pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { self.channel_paths.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index d9880496f63b5060a9bf249bfe487e2ff5a49a51..db98c6abdc1138c125cd25434aad1aac093ce8a4 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,6 +1,5 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -use channel::channel_buffer::ChannelBuffer; use client::UserId; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; @@ -22,8 +21,9 @@ async fn test_core_channel_buffers( .await; // Client A joins the channel buffer - let channel_buffer_a = cx_a - .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) .await .unwrap(); @@ -45,8 +45,9 @@ async fn test_core_channel_buffers( assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); // Client B joins the channel buffer - let channel_buffer_b = cx_b - .update(|cx| ChannelBuffer::join_channel(zed_id, client_b.client().to_owned(), cx)) + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx)) .await .unwrap(); @@ -79,8 +80,9 @@ async fn test_core_channel_buffers( }); // Client A rejoins the channel buffer - let _channel_buffer_a = cx_a - .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) + let _channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channels, cx| channels.open_channel_buffer(zed_id, cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -104,7 +106,8 @@ async fn test_core_channel_buffers( }); // TODO: - // - Test synchronizing offline updates, what happens to A's channel buffer? + // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects + // - Test interaction with channel deletion while buffer is open } #[track_caller] diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index e0177f660909d0c6deaa07fb8b80f822f243b9dc..1ecb4b84227b066ab959997c128bfd96cec6055d 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -34,6 +34,7 @@ editor = { path = "../editor" } feedback = { path = "../feedback" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +language = { path = "../language" } menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..27a2d678f51d0298433ae3fce44220c0cd37b07f --- /dev/null +++ b/crates/collab_ui/src/channel_view.rs @@ -0,0 +1,69 @@ +use channel::channel_buffer::ChannelBuffer; +use editor::Editor; +use gpui::{ + actions, + elements::{ChildView, Label}, + AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, +}; +use language::Language; +use std::sync::Arc; +use workspace::item::{Item, ItemHandle}; + +actions!(channel_view, [Deploy]); + +pub(crate) fn init(cx: &mut AppContext) { + // TODO +} + +pub struct ChannelView { + editor: ViewHandle, + channel_buffer: ModelHandle, +} + +impl ChannelView { + pub fn new( + channel_buffer: ModelHandle, + language: Arc, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx)); + let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + Self { + editor, + channel_buffer, + } + } +} + +impl Entity for ChannelView { + type Event = editor::Event; +} + +impl View for ChannelView { + fn ui_name() -> &'static str { + "ChannelView" + } + + fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement { + ChildView::new(self.editor.as_any(), cx).into_any() + } +} + +impl Item for ChannelView { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + cx: &gpui::AppContext, + ) -> AnyElement { + let channel_name = self + .channel_buffer + .read(cx) + .channel(cx) + .map_or("[Deleted channel]".to_string(), |channel| { + format!("#{}", channel.name) + }); + Label::new(channel_name, style.label.to_owned()).into_any() + } +} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ab692dd1667173b2c45e47c0cec634a7f8bdb38b..0eb6a659842dd66e3370508f47761ab0f1142fa7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -42,7 +42,10 @@ use workspace::{ Workspace, }; -use crate::face_pile::FacePile; +use crate::{ + channel_view::{self, ChannelView}, + face_pile::FacePile, +}; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -77,6 +80,11 @@ struct RenameChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct OpenChannelBuffer { + channel_id: u64, +} + actions!( collab_panel, [ @@ -96,7 +104,8 @@ impl_actions!( InviteMembers, ManageMembers, RenameChannel, - ToggleCollapse + ToggleCollapse, + OpenChannelBuffer ] ); @@ -106,6 +115,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); contact_finder::init(cx); channel_modal::init(cx); + channel_view::init(cx); cx.add_action(CollabPanel::cancel); cx.add_action(CollabPanel::select_next); @@ -121,7 +131,8 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::rename_channel); cx.add_action(CollabPanel::toggle_channel_collapsed); cx.add_action(CollabPanel::collapse_selected_channel); - cx.add_action(CollabPanel::expand_selected_channel) + cx.add_action(CollabPanel::expand_selected_channel); + cx.add_action(CollabPanel::open_channel_buffer); } #[derive(Debug)] @@ -1888,6 +1899,7 @@ impl CollabPanel { vec![ ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), ContextMenuItem::Separator, ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), ContextMenuItem::Separator, @@ -2207,6 +2219,34 @@ impl CollabPanel { } } + fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { + let workspace = self.workspace; + let open = self.channel_store.update(cx, |channel_store, cx| { + channel_store.open_channel_buffer(action.channel_id, cx) + }); + + cx.spawn(|_, mut cx| async move { + let channel_buffer = open.await?; + + let markdown = workspace + .read_with(&cx, |workspace, _| { + workspace + .app_state() + .languages + .language_for_name("Markdown") + })? + .await?; + + workspace.update(&mut cx, |workspace, cx| { + let channel_view = cx.add_view(|cx| ChannelView::new(channel_buffer, markdown, cx)); + workspace.add_item(Box::new(channel_view), cx); + })?; + + anyhow::Ok(()) + }) + .detach(); + } + fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { let Some(channel) = self.selected_channel() else { return; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 5420dd1db5733882f8f08b939f2bc15ecaf83949..04644b62d985698dcebfdfea352cc3cb2e15f824 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +pub mod channel_view; pub mod collab_panel; mod collab_titlebar_item; mod contact_notification; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 03625c80e700ca089ade1d5d06f24781d600f681..890bd55a7f0b83e36557675a7b9384a4531399f2 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4687,12 +4687,13 @@ impl AnyWeakModelHandle { } } -#[derive(Copy)] pub struct WeakViewHandle { any_handle: AnyWeakViewHandle, view_type: PhantomData, } +impl Copy for WeakViewHandle {} + impl Debug for WeakViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(&format!("WeakViewHandle<{}>", type_name::())) From 1d08f44e702024e26753388c538b93d5fafcd8fd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 15:33:37 -0700 Subject: [PATCH 030/142] Snapshot channel notes buffers when everyone leaves Co-authored-by: Mikayla --- Cargo.lock | 1 + crates/collab/Cargo.toml | 1 + .../20221109000000_test_schema.sql | 1 + .../20230819154600_add_channel_buffers.sql | 1 + crates/collab/src/db/queries/buffers.rs | 352 +++++++++++++----- .../collab/src/db/tables/buffer_snapshot.rs | 1 + crates/collab/src/db/tests/buffer_tests.rs | 10 +- crates/language/src/proto.rs | 1 + crates/text/src/text.rs | 2 +- 9 files changed, 273 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index deed9a176ece67f0253b3babd2c7feb32115a26d..0ec2f3418535b4da0d4917b3221d19f26087b1da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1458,6 +1458,7 @@ dependencies = [ "channel", "clap 3.2.25", "client", + "clock", "collections", "ctor", "dashmap", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index fc78a03f6777046dc88377515d5cd3d63fe57374..cc1970266deb39e2f045cab0ebf9c24e205539af 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -14,6 +14,7 @@ name = "seed" required-features = ["seed-support"] [dependencies] +clock = { path = "../clock" } collections = { path = "../collections" } live_kit_server = { path = "../live_kit_server" } text = { path = "../text" } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index f39f0cca5909f28e11b91b57d73fc8d0cd5c8a47..fdae4f23395f095cd2265ad7eb0a74b05ea4f078 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -233,6 +233,7 @@ CREATE TABLE "buffer_snapshots" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "text" TEXT NOT NULL, + "operation_serialization_version" INTEGER NOT NULL, PRIMARY KEY(buffer_id, epoch) ); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql index f6bd2879c618ac2851a01b545f1f8eb8ec7c746d..fec18ddb8d3732af7d17c0f7e3a4ee39e7f09e53 100644 --- a/crates/collab/migrations/20230819154600_add_channel_buffers.sql +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -22,6 +22,7 @@ CREATE TABLE "buffer_snapshots" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "text" TEXT NOT NULL, + "operation_serialization_version" INTEGER NOT NULL, PRIMARY KEY(buffer_id, epoch) ); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 7f0e5a75f06b2d297059855747545e37089b0164..b0df905ecb80de238b7c8734b04b17c6475e652b 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,5 +1,7 @@ use super::*; use prost::Message; +use std::ops::Range; +use text::{EditOperation, InsertionTimestamp, UndoOperation}; impl Database { pub async fn join_channel_buffer( @@ -31,6 +33,16 @@ impl Database { } .insert(&*tx) .await?; + buffer_snapshot::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(0), + text: ActiveValue::Set(String::new()), + operation_serialization_version: ActiveValue::Set( + storage::SERIALIZATION_VERSION, + ), + } + .insert(&*tx) + .await?; buffer }; @@ -60,58 +72,7 @@ impl Database { collaborators.push(collaborator); // Assemble the buffer state - let id = buffer.id; - let base_text = if buffer.epoch > 0 { - buffer_snapshot::Entity::find() - .filter( - buffer_snapshot::Column::BufferId - .eq(id) - .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such snapshot"))? - .text - } else { - String::new() - }; - - let mut rows = buffer_operation::Entity::find() - .filter( - buffer_operation::Column::BufferId - .eq(id) - .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), - ) - .stream(&*tx) - .await?; - let mut operations = Vec::new(); - while let Some(row) = rows.next().await { - let row = row?; - let version = deserialize_version(&row.version)?; - let operation = if row.is_undo { - let counts = deserialize_undo_operation(&row.value)?; - proto::operation::Variant::Undo(proto::operation::Undo { - replica_id: row.replica_id as u32, - local_timestamp: row.local_timestamp as u32, - lamport_timestamp: row.lamport_timestamp as u32, - version, - counts, - }) - } else { - let (ranges, new_text) = deserialize_edit_operation(&row.value)?; - proto::operation::Variant::Edit(proto::operation::Edit { - replica_id: row.replica_id as u32, - local_timestamp: row.local_timestamp as u32, - lamport_timestamp: row.lamport_timestamp as u32, - version, - ranges, - new_text, - }) - }; - operations.push(proto::Operation { - variant: Some(operation), - }) - } + let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?; Ok(proto::JoinChannelBufferResponse { buffer_id: buffer.id.to_proto(), @@ -180,6 +141,12 @@ impl Database { }); } + drop(rows); + + if connections.is_empty() { + self.snapshot_buffer(channel_id, &tx).await?; + } + Ok(connections) } @@ -258,42 +225,23 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("no such buffer"))?; - let buffer_id = buffer.id; + + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryVersion { + OperationSerializationVersion, + } + + let serialization_version: i32 = buffer + .find_related(buffer_snapshot::Entity) + .select_only() + .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch)) + .into_values::<_, QueryVersion>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("missing buffer snapshot"))?; + buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { - match operation.variant.as_ref()? { - proto::operation::Variant::Edit(operation) => { - let value = - serialize_edit_operation(&operation.ranges, &operation.new_text); - let version = serialize_version(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer_id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(false), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::Undo(operation) => { - let value = serialize_undo_operation(&operation.counts); - let version = serialize_version(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer_id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(true), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::UpdateSelections(_) => None, - proto::operation::Variant::UpdateDiagnostics(_) => None, - proto::operation::Variant::UpdateCompletionTriggers(_) => None, - } + operation_to_storage(operation, &buffer, serialization_version) })) .exec(&*tx) .await?; @@ -318,6 +266,222 @@ impl Database { }) .await } + + async fn get_buffer_state( + &self, + buffer: &buffer::Model, + tx: &DatabaseTransaction, + ) -> Result<(String, Vec)> { + let id = buffer.id; + let (base_text, version) = if buffer.epoch > 0 { + let snapshot = buffer_snapshot::Entity::find() + .filter( + buffer_snapshot::Column::BufferId + .eq(id) + .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such snapshot"))?; + + let version = snapshot.operation_serialization_version; + (snapshot.text, version) + } else { + (String::new(), storage::SERIALIZATION_VERSION) + }; + + let mut rows = buffer_operation::Entity::find() + .filter( + buffer_operation::Column::BufferId + .eq(id) + .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), + ) + .stream(&*tx) + .await?; + let mut operations = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + + let operation = operation_from_storage(row, version)?; + operations.push(proto::Operation { + variant: Some(operation), + }) + } + + Ok((base_text, operations)) + } + + async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> { + let buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + + let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; + + let mut text_buffer = text::Buffer::new(0, 0, base_text); + + text_buffer + .apply_ops( + operations + .into_iter() + .filter_map(deserialize_wire_operation), + ) + .unwrap(); + + let base_text = text_buffer.text(); + let epoch = buffer.epoch + 1; + + buffer_snapshot::Model { + buffer_id: buffer.id, + epoch, + text: base_text, + operation_serialization_version: storage::SERIALIZATION_VERSION, + } + .into_active_model() + .insert(tx) + .await?; + + buffer::ActiveModel { + id: ActiveValue::Unchanged(buffer.id), + epoch: ActiveValue::Set(epoch), + ..Default::default() + } + .save(tx) + .await?; + + Ok(()) + } +} + +fn operation_to_storage( + operation: &proto::Operation, + buffer: &buffer::Model, + _format: i32, +) -> Option { + match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => { + let value = edit_operation_to_storage(&operation.ranges, &operation.new_text); + let version = version_to_storage(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(false), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::Undo(operation) => { + let value = undo_operation_to_storage(&operation.counts); + let version = version_to_storage(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(true), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::UpdateSelections(_) => None, + proto::operation::Variant::UpdateDiagnostics(_) => None, + proto::operation::Variant::UpdateCompletionTriggers(_) => None, + } +} + +fn operation_from_storage( + row: buffer_operation::Model, + _format_version: i32, +) -> Result { + let version = version_from_storage(&row.version)?; + let operation = if row.is_undo { + let counts = undo_operation_from_storage(&row.value)?; + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: row.replica_id as u32, + local_timestamp: row.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + counts, + }) + } else { + let (ranges, new_text) = edit_operation_from_storage(&row.value)?; + proto::operation::Variant::Edit(proto::operation::Edit { + replica_id: row.replica_id as u32, + local_timestamp: row.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + ranges, + new_text, + }) + }; + Ok(operation) +} + +// This is currently a manual copy of the deserialization code in the client's langauge crate +pub fn deserialize_wire_operation(operation: proto::Operation) -> Option { + match operation.variant? { + proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { + timestamp: InsertionTimestamp { + replica_id: edit.replica_id as text::ReplicaId, + local: edit.local_timestamp, + lamport: edit.lamport_timestamp, + }, + version: deserialize_wire_version(&edit.version), + ranges: edit.ranges.into_iter().map(deserialize_range).collect(), + new_text: edit.new_text.into_iter().map(Arc::from).collect(), + })), + proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo { + lamport_timestamp: clock::Lamport { + replica_id: undo.replica_id as text::ReplicaId, + value: undo.lamport_timestamp, + }, + undo: UndoOperation { + id: clock::Local { + replica_id: undo.replica_id as text::ReplicaId, + value: undo.local_timestamp, + }, + version: deserialize_wire_version(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Local { + replica_id: c.replica_id as text::ReplicaId, + value: c.local_timestamp, + }, + c.count, + ) + }) + .collect(), + }, + }), + _ => None, + } +} + +pub fn deserialize_range(range: proto::Range) -> Range { + text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize) +} + +fn deserialize_wire_version(message: &[proto::VectorClockEntry]) -> clock::Global { + let mut version = clock::Global::new(); + for entry in message { + version.observe(clock::Local { + replica_id: entry.replica_id as text::ReplicaId, + value: entry.timestamp, + }); + } + version } mod storage { @@ -325,7 +489,7 @@ mod storage { use prost::Message; - pub const VERSION: usize = 1; + pub const SERIALIZATION_VERSION: i32 = 1; #[derive(Message)] pub struct VectorClock { @@ -374,7 +538,7 @@ mod storage { } } -fn serialize_version(version: &Vec) -> Vec { +fn version_to_storage(version: &Vec) -> Vec { storage::VectorClock { entries: version .iter() @@ -387,7 +551,7 @@ fn serialize_version(version: &Vec) -> Vec { .encode_to_vec() } -fn deserialize_version(bytes: &[u8]) -> Result> { +fn version_from_storage(bytes: &[u8]) -> Result> { let clock = storage::VectorClock::decode(bytes).map_err(|error| anyhow!("{}", error))?; Ok(clock .entries @@ -399,7 +563,7 @@ fn deserialize_version(bytes: &[u8]) -> Result> { .collect()) } -fn serialize_edit_operation(ranges: &[proto::Range], texts: &[String]) -> Vec { +fn edit_operation_to_storage(ranges: &[proto::Range], texts: &[String]) -> Vec { storage::TextEdit { ranges: ranges .iter() @@ -413,7 +577,7 @@ fn serialize_edit_operation(ranges: &[proto::Range], texts: &[String]) -> Vec Result<(Vec, Vec)> { +fn edit_operation_from_storage(bytes: &[u8]) -> Result<(Vec, Vec)> { let edit = storage::TextEdit::decode(bytes).map_err(|error| anyhow!("{}", error))?; let ranges = edit .ranges @@ -426,7 +590,7 @@ fn deserialize_edit_operation(bytes: &[u8]) -> Result<(Vec, Vec) -> Vec { +fn undo_operation_to_storage(counts: &Vec) -> Vec { storage::Undo { entries: counts .iter() @@ -440,7 +604,7 @@ fn serialize_undo_operation(counts: &Vec) -> Vec { .encode_to_vec() } -fn deserialize_undo_operation(bytes: &[u8]) -> Result> { +fn undo_operation_from_storage(bytes: &[u8]) -> Result> { let undo = storage::Undo::decode(bytes).map_err(|error| anyhow!("{}", error))?; Ok(undo .entries diff --git a/crates/collab/src/db/tables/buffer_snapshot.rs b/crates/collab/src/db/tables/buffer_snapshot.rs index ca8712a053db2190a8280e10d8313dec92099adf..c9de665e438ef373e43b02cc98e7b3ec61640267 100644 --- a/crates/collab/src/db/tables/buffer_snapshot.rs +++ b/crates/collab/src/db/tables/buffer_snapshot.rs @@ -9,6 +9,7 @@ pub struct Model { #[sea_orm(primary_key)] pub epoch: i32, pub text: String, + pub operation_serialization_version: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 08252e382eef8dc7166275acfad398e7ffeb121a..e71748b88b0571c2ccd7840924b8fc325cd368b7 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -10,7 +10,6 @@ test_both_dbs!( ); async fn test_channel_buffers(db: &Arc) { - // Prep database test info let a_id = db .create_user( "user_a@example.com", @@ -155,5 +154,12 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(zed_collaborators, &[]); assert_eq!(cargo_collaborators, &[]); - // TODO: test buffer epoch incrementing + // When everyone has left the channel, the operations are collapsed into + // a new base text. + let buffer_response_b = db + .join_channel_buffer(zed_id, b_id, connection_id_b) + .await + .unwrap(); + assert_eq!(buffer_response_b.base_text, "hello, cruel world"); + assert_eq!(buffer_response_b.operations, &[]); } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 0de3f704c7f29e4d1cff8f3fe371d0fad36d1f42..09c5ec7fc3214290b39eeb3585455839a883dbde 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -207,6 +207,7 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { } } +// This behavior is currently copied in the collab database, for snapshotting channel notes pub fn deserialize_operation(message: proto::Operation) -> Result { Ok( match message diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 7c94f25e1eb73c125e61effef56565cb9597bd85..4a97faf01515317855bfb8fdea1ac33fb7814b41 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -12,7 +12,7 @@ mod undo_map; pub use anchor::*; use anyhow::{anyhow, Result}; -use clock::ReplicaId; +pub use clock::ReplicaId; use collections::{HashMap, HashSet}; use fs::LineEnding; use locator::Locator; From 11ef5e27407703e312107d353bb933cbd833d44d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 16:42:08 -0700 Subject: [PATCH 031/142] Simplify buffer_operations schema Co-authored-by: Mikayla --- .../20221109000000_test_schema.sql | 3 - .../20230819154600_add_channel_buffers.sql | 3 - crates/collab/src/db/queries/buffers.rs | 278 ++++++++---------- .../collab/src/db/tables/buffer_operation.rs | 3 - 4 files changed, 121 insertions(+), 166 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index fdae4f23395f095cd2265ad7eb0a74b05ea4f078..7a4cd9fd23cbc80bb38e3b2e7446ae53a902066a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -222,9 +222,6 @@ CREATE TABLE "buffer_operations" ( "epoch" INTEGER NOT NULL, "replica_id" INTEGER NOT NULL, "lamport_timestamp" INTEGER NOT NULL, - "local_timestamp" INTEGER NOT NULL, - "version" BLOB NOT NULL, - "is_undo" BOOLEAN NOT NULL, "value" BLOB NOT NULL, PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) ); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql index fec18ddb8d3732af7d17c0f7e3a4ee39e7f09e53..5e6e7ce3393a628c86cbcdabf2349ebfa6667bd6 100644 --- a/crates/collab/migrations/20230819154600_add_channel_buffers.sql +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -10,10 +10,7 @@ CREATE TABLE "buffer_operations" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "replica_id" INTEGER NOT NULL, - "local_timestamp" INTEGER NOT NULL, "lamport_timestamp" INTEGER NOT NULL, - "version" BYTEA NOT NULL, - "is_undo" BOOLEAN NOT NULL, "value" BYTEA NOT NULL, PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) ); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index b0df905ecb80de238b7c8734b04b17c6475e652b..83f5b87079416259f673f1ddeedad4e31e72aa21 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,6 +1,5 @@ use super::*; use prost::Message; -use std::ops::Range; use text::{EditOperation, InsertionTimestamp, UndoOperation}; impl Database { @@ -234,6 +233,7 @@ impl Database { let serialization_version: i32 = buffer .find_related(buffer_snapshot::Entity) .select_only() + .column(buffer_snapshot::Column::OperationSerializationVersion) .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch)) .into_values::<_, QueryVersion>() .one(&*tx) @@ -326,11 +326,7 @@ impl Database { let mut text_buffer = text::Buffer::new(0, 0, base_text); text_buffer - .apply_ops( - operations - .into_iter() - .filter_map(deserialize_wire_operation), - ) + .apply_ops(operations.into_iter().filter_map(operation_from_wire)) .unwrap(); let base_text = text_buffer.text(); @@ -363,71 +359,122 @@ fn operation_to_storage( buffer: &buffer::Model, _format: i32, ) -> Option { - match operation.variant.as_ref()? { - proto::operation::Variant::Edit(operation) => { - let value = edit_operation_to_storage(&operation.ranges, &operation.new_text); - let version = version_to_storage(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer.id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(false), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::Undo(operation) => { - let value = undo_operation_to_storage(&operation.counts); - let version = version_to_storage(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer.id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(true), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::UpdateSelections(_) => None, - proto::operation::Variant::UpdateDiagnostics(_) => None, - proto::operation::Variant::UpdateCompletionTriggers(_) => None, - } + let (replica_id, lamport_timestamp, value) = match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => ( + operation.replica_id, + operation.lamport_timestamp, + storage::Operation { + local_timestamp: operation.local_timestamp, + version: version_to_storage(&operation.version), + is_undo: false, + edit_ranges: operation + .ranges + .iter() + .map(|range| storage::Range { + start: range.start, + end: range.end, + }) + .collect(), + edit_texts: operation.new_text.clone(), + undo_counts: Vec::new(), + }, + ), + proto::operation::Variant::Undo(operation) => ( + operation.replica_id, + operation.lamport_timestamp, + storage::Operation { + local_timestamp: operation.local_timestamp, + version: version_to_storage(&operation.version), + is_undo: true, + edit_ranges: Vec::new(), + edit_texts: Vec::new(), + undo_counts: operation + .counts + .iter() + .map(|entry| storage::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), + }, + ), + _ => None?, + }; + + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(replica_id as i32), + lamport_timestamp: ActiveValue::Set(lamport_timestamp as i32), + value: ActiveValue::Set(value.encode_to_vec()), + }) } fn operation_from_storage( row: buffer_operation::Model, _format_version: i32, ) -> Result { - let version = version_from_storage(&row.version)?; - let operation = if row.is_undo { - let counts = undo_operation_from_storage(&row.value)?; + let operation = + storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?; + let version = version_from_storage(&operation.version); + Ok(if operation.is_undo { proto::operation::Variant::Undo(proto::operation::Undo { replica_id: row.replica_id as u32, - local_timestamp: row.local_timestamp as u32, + local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, - counts, + counts: operation + .undo_counts + .iter() + .map(|entry| proto::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), }) } else { - let (ranges, new_text) = edit_operation_from_storage(&row.value)?; proto::operation::Variant::Edit(proto::operation::Edit { replica_id: row.replica_id as u32, - local_timestamp: row.local_timestamp as u32, + local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, - ranges, - new_text, + ranges: operation + .edit_ranges + .into_iter() + .map(|range| proto::Range { + start: range.start, + end: range.end, + }) + .collect(), + new_text: operation.edit_texts, }) - }; - Ok(operation) + }) +} + +fn version_to_storage(version: &Vec) -> Vec { + version + .iter() + .map(|entry| storage::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect() +} + +fn version_from_storage(version: &Vec) -> Vec { + version + .iter() + .map(|entry| proto::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect() } // This is currently a manual copy of the deserialization code in the client's langauge crate -pub fn deserialize_wire_operation(operation: proto::Operation) -> Option { +pub fn operation_from_wire(operation: proto::Operation) -> Option { match operation.variant? { proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { timestamp: InsertionTimestamp { @@ -435,8 +482,14 @@ pub fn deserialize_wire_operation(operation: proto::Operation) -> Option Some(text::Operation::Undo { @@ -449,7 +502,7 @@ pub fn deserialize_wire_operation(operation: proto::Operation) -> Option Option Range { - text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize) -} - -fn deserialize_wire_version(message: &[proto::VectorClockEntry]) -> clock::Global { +fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global { let mut version = clock::Global::new(); for entry in message { version.observe(clock::Local { @@ -486,15 +535,23 @@ fn deserialize_wire_version(message: &[proto::VectorClockEntry]) -> clock::Globa mod storage { #![allow(non_snake_case)] - use prost::Message; - pub const SERIALIZATION_VERSION: i32 = 1; #[derive(Message)] - pub struct VectorClock { - #[prost(message, repeated, tag = "1")] - pub entries: Vec, + pub struct Operation { + #[prost(uint32, tag = "1")] + pub local_timestamp: u32, + #[prost(message, repeated, tag = "2")] + pub version: Vec, + #[prost(bool, tag = "3")] + pub is_undo: bool, + #[prost(message, repeated, tag = "4")] + pub edit_ranges: Vec, + #[prost(string, repeated, tag = "5")] + pub edit_texts: Vec, + #[prost(message, repeated, tag = "6")] + pub undo_counts: Vec, } #[derive(Message)] @@ -505,14 +562,6 @@ mod storage { pub timestamp: u32, } - #[derive(Message)] - pub struct TextEdit { - #[prost(message, repeated, tag = "1")] - pub ranges: Vec, - #[prost(string, repeated, tag = "2")] - pub texts: Vec, - } - #[derive(Message)] pub struct Range { #[prost(uint64, tag = "1")] @@ -521,12 +570,6 @@ mod storage { pub end: u64, } - #[derive(Message)] - pub struct Undo { - #[prost(message, repeated, tag = "1")] - pub entries: Vec, - } - #[derive(Message)] pub struct UndoCount { #[prost(uint32, tag = "1")] @@ -537,82 +580,3 @@ mod storage { pub count: u32, } } - -fn version_to_storage(version: &Vec) -> Vec { - storage::VectorClock { - entries: version - .iter() - .map(|entry| storage::VectorClockEntry { - replica_id: entry.replica_id, - timestamp: entry.timestamp, - }) - .collect(), - } - .encode_to_vec() -} - -fn version_from_storage(bytes: &[u8]) -> Result> { - let clock = storage::VectorClock::decode(bytes).map_err(|error| anyhow!("{}", error))?; - Ok(clock - .entries - .into_iter() - .map(|entry| proto::VectorClockEntry { - replica_id: entry.replica_id, - timestamp: entry.timestamp, - }) - .collect()) -} - -fn edit_operation_to_storage(ranges: &[proto::Range], texts: &[String]) -> Vec { - storage::TextEdit { - ranges: ranges - .iter() - .map(|range| storage::Range { - start: range.start, - end: range.end, - }) - .collect(), - texts: texts.to_vec(), - } - .encode_to_vec() -} - -fn edit_operation_from_storage(bytes: &[u8]) -> Result<(Vec, Vec)> { - let edit = storage::TextEdit::decode(bytes).map_err(|error| anyhow!("{}", error))?; - let ranges = edit - .ranges - .into_iter() - .map(|range| proto::Range { - start: range.start, - end: range.end, - }) - .collect(); - Ok((ranges, edit.texts)) -} - -fn undo_operation_to_storage(counts: &Vec) -> Vec { - storage::Undo { - entries: counts - .iter() - .map(|entry| storage::UndoCount { - replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, - count: entry.count, - }) - .collect(), - } - .encode_to_vec() -} - -fn undo_operation_from_storage(bytes: &[u8]) -> Result> { - let undo = storage::Undo::decode(bytes).map_err(|error| anyhow!("{}", error))?; - Ok(undo - .entries - .iter() - .map(|entry| proto::UndoCount { - replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, - count: entry.count, - }) - .collect()) -} diff --git a/crates/collab/src/db/tables/buffer_operation.rs b/crates/collab/src/db/tables/buffer_operation.rs index 59626c1e77f2ba80b828a5cede6a8dcbfc8dbefe..37bd4bedfebf1d0090e27017d19bf22ea8fa38b5 100644 --- a/crates/collab/src/db/tables/buffer_operation.rs +++ b/crates/collab/src/db/tables/buffer_operation.rs @@ -12,9 +12,6 @@ pub struct Model { pub lamport_timestamp: i32, #[sea_orm(primary_key)] pub replica_id: i32, - pub local_timestamp: i32, - pub version: Vec, - pub is_undo: bool, pub value: Vec, } From 7e831388050d2e4b026421b3100bf71fa9ef22cc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 18:08:03 -0700 Subject: [PATCH 032/142] Start work on showing consistent replica ids for channel buffers Co-authored-by: Mikayla --- Cargo.lock | 1 + crates/channel/src/channel_buffer.rs | 4 + crates/collab/Cargo.toml | 1 + .../collab/src/tests/channel_buffer_tests.rs | 136 +++++++++++++++++- crates/collab_ui/src/channel_view.rs | 43 +++++- crates/collab_ui/src/collab_panel.rs | 9 +- crates/editor/src/editor.rs | 11 ++ crates/project/src/project.rs | 4 +- 8 files changed, 202 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ec2f3418535b4da0d4917b3221d19f26087b1da..2bda9fda466d2c553ca8809eb09957e6169de12c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,6 +1459,7 @@ dependencies = [ "clap 3.2.25", "client", "clock", + "collab_ui", "collections", "ctor", "dashmap", diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index aa99d5c10b34865e98a6424bb1683699bddd9855..5ee3fd6c849465f0d0f3288317b5798dd0e683a8 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -171,4 +171,8 @@ impl ChannelBuffer { .channel_for_id(self.channel_id) .cloned() } + + pub fn replica_id(&self, cx: &AppContext) -> u16 { + self.buffer.read(cx).replica_id() + } } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index cc1970266deb39e2f045cab0ebf9c24e205539af..8adc38615c3cab87d36a348323e0c3674f555d6c 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -78,6 +78,7 @@ rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } +collab_ui = { path = "../collab_ui", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index db98c6abdc1138c125cd25434aad1aac093ce8a4..8fb50055f5a46c0409b4ed94d0a5653da02579d2 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,8 +1,11 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; - +use call::ActiveCall; use client::UserId; +use collab_ui::channel_view::ChannelView; +use collections::HashMap; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; +use serde_json::json; use std::sync::Arc; #[gpui::test] @@ -82,7 +85,9 @@ async fn test_core_channel_buffers( // Client A rejoins the channel buffer let _channel_buffer_a = client_a .channel_store() - .update(cx_a, |channels, cx| channels.open_channel_buffer(zed_id, cx)) + .update(cx_a, |channels, cx| { + channels.open_channel_buffer(zed_id, cx) + }) .await .unwrap(); deterministic.run_until_parked(); @@ -110,6 +115,133 @@ async fn test_core_channel_buffers( // - Test interaction with channel deletion while buffer is open } +#[gpui::test] +async fn test_channel_buffer_replica_ids( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_id = server + .make_channel( + "zed", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + // Clients A and B join a channel. + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + + // Clients A, B, and C join a channel buffer + // C first so that the replica IDs in the project and the channel buffer are different + let channel_buffer_c = client_c + .channel_store() + .update(cx_c, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + // Client B shares a project + client_b + .fs() + .insert_tree("/dir", json!({ "file.txt": "contents" })) + .await; + let (project_b, _) = client_b.build_local_project("/dir", cx_b).await; + let shared_project_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + // Client A joins the project + let project_a = client_a.build_remote_project(shared_project_id, cx_a).await; + deterministic.run_until_parked(); + + // Client C is in a separate project. + client_c.fs().insert_tree("/dir", json!({})).await; + let (project_c, _) = client_c.build_local_project("/dir", cx_c).await; + + // Note that each user has a different replica id in the projects vs the + // channel buffer. + channel_buffer_a.read_with(cx_a, |channel_buffer, cx| { + assert_eq!(project_a.read(cx).replica_id(), 1); + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2); + }); + channel_buffer_b.read_with(cx_b, |channel_buffer, cx| { + assert_eq!(project_b.read(cx).replica_id(), 0); + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1); + }); + channel_buffer_c.read_with(cx_c, |channel_buffer, cx| { + // C is not in the project + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0); + }); + + let channel_window_a = cx_a + .add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), None, cx)); + let channel_window_b = cx_b + .add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), None, cx)); + let channel_window_c = cx_c + .add_window(|cx| ChannelView::new(project_c.clone(), channel_buffer_c.clone(), None, cx)); + + let channel_view_a = channel_window_a.root(cx_a); + let channel_view_b = channel_window_b.root(cx_b); + let channel_view_c = channel_window_c.root(cx_c); + + // For clients A and B, the replica ids in the channel buffer are mapped + // so that they match the same users' replica ids in their shared project. + channel_view_a.read_with(cx_a, |view, cx| { + assert_eq!( + view.project_replica_ids_by_channel_buffer_replica_id(cx), + [(1, 0), (2, 1)].into_iter().collect::>() + ); + }); + channel_view_b.read_with(cx_b, |view, cx| { + assert_eq!( + view.project_replica_ids_by_channel_buffer_replica_id(cx), + [(1, 0), (2, 1)].into_iter().collect::>(), + ) + }); + + // Client C only sees themself, as they're not part of any shared project + channel_view_c.read_with(cx_c, |view, cx| { + assert_eq!( + view.project_replica_ids_by_channel_buffer_replica_id(cx), + [(0, 0)].into_iter().collect::>(), + ); + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 27a2d678f51d0298433ae3fce44220c0cd37b07f..af45eabe690fd678867be093c58c6bedd86c0271 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,4 +1,6 @@ use channel::channel_buffer::ChannelBuffer; +use clock::ReplicaId; +use collections::HashMap; use editor::Editor; use gpui::{ actions, @@ -6,6 +8,7 @@ use gpui::{ AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, }; use language::Language; +use project::Project; use std::sync::Arc; use workspace::item::{Item, ItemHandle}; @@ -17,22 +20,56 @@ pub(crate) fn init(cx: &mut AppContext) { pub struct ChannelView { editor: ViewHandle, + project: ModelHandle, channel_buffer: ModelHandle, } impl ChannelView { pub fn new( + project: ModelHandle, channel_buffer: ModelHandle, - language: Arc, + language: Option>, cx: &mut ViewContext, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); - buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx)); + buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); - Self { + let this = Self { editor, + project, channel_buffer, + }; + let mapping = this.project_replica_ids_by_channel_buffer_replica_id(cx); + this.editor + .update(cx, |editor, cx| editor.set_replica_id_mapping(mapping, cx)); + this + } + + /// Channel Buffer Replica ID -> Project Replica ID + pub fn project_replica_ids_by_channel_buffer_replica_id( + &self, + cx: &AppContext, + ) -> HashMap { + let project = self.project.read(cx); + let mut result = HashMap::default(); + result.insert( + self.channel_buffer.read(cx).replica_id(cx), + project.replica_id(), + ); + for collaborator in self.channel_buffer.read(cx).collaborators() { + let project_replica_id = + project + .collaborators() + .values() + .find_map(|project_collaborator| { + (project_collaborator.user_id == collaborator.user_id) + .then_some(project_collaborator.replica_id) + }); + if let Some(project_replica_id) = project_replica_id { + result.insert(collaborator.replica_id as ReplicaId, project_replica_id); + } } + result } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0eb6a659842dd66e3370508f47761ab0f1142fa7..a6bd09e43bd2a31877914da1c221f702ff7cc230 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2238,7 +2238,14 @@ impl CollabPanel { .await?; workspace.update(&mut cx, |workspace, cx| { - let channel_view = cx.add_view(|cx| ChannelView::new(channel_buffer, markdown, cx)); + let channel_view = cx.add_view(|cx| { + ChannelView::new( + workspace.project().to_owned(), + channel_buffer, + Some(markdown), + cx, + ) + }); workspace.add_item(Box::new(channel_view), cx); })?; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 67279b1ba62551d0b41b7dc97a3d1e56eb841691..e7197d98c5cacc3d1eb7f47a8c968d239182cb7e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -559,6 +559,7 @@ pub struct Editor { blink_manager: ModelHandle, show_local_selections: bool, mode: EditorMode, + replica_id_mapping: Option>, show_gutter: bool, show_wrap_guides: Option, placeholder_text: Option>, @@ -1394,6 +1395,7 @@ impl Editor { blink_manager: blink_manager.clone(), show_local_selections: true, mode, + replica_id_mapping: None, show_gutter: mode == EditorMode::Full, show_wrap_guides: None, placeholder_text: None, @@ -1604,6 +1606,15 @@ impl Editor { self.read_only = read_only; } + pub fn set_replica_id_mapping( + &mut self, + mapping: HashMap, + cx: &mut ViewContext, + ) { + self.replica_id_mapping = Some(mapping); + cx.notify(); + } + fn selections_did_change( &mut self, local: bool, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b120baa9512c5610e0a1e217e9c88ac536e7eec7..bc4fa587cb83974a3ca16fff38c8e16e9ca169ff 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,7 +11,7 @@ mod project_tests; mod worktree_tests; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, TypedEnvelope, UserStore}; +use client::{proto, Client, TypedEnvelope, UserId, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use copilot::Copilot; @@ -250,6 +250,7 @@ enum ProjectClientState { pub struct Collaborator { pub peer_id: proto::PeerId, pub replica_id: ReplicaId, + pub user_id: UserId, } #[derive(Clone, Debug, PartialEq)] @@ -7756,6 +7757,7 @@ impl Collaborator { Ok(Self { peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?, replica_id: message.replica_id as ReplicaId, + user_id: message.user_id as UserId, }) } } From e4794e3134b6449e36ed2771a8849046489cc252 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 23 Aug 2023 23:45:42 -0600 Subject: [PATCH 033/142] vim: Fix linewise copy of last line with no trailing newline Along the way, delete the VimBindingTestContext by updating the visual tests to no-longer need it. --- crates/vim/src/test.rs | 2 - .../vim/src/test/vim_binding_test_context.rs | 64 ------- crates/vim/src/test/vim_test_context.rs | 10 -- crates/vim/src/utils.rs | 19 +- crates/vim/src/visual.rs | 164 ++++++++---------- .../test_data/test_visual_line_delete.json | 15 +- crates/vim/test_data/test_visual_yank.json | 29 ++++ 7 files changed, 117 insertions(+), 186 deletions(-) delete mode 100644 crates/vim/src/test/vim_binding_test_context.rs create mode 100644 crates/vim/test_data/test_visual_yank.json diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 3b1b8c24f2d301f5f843bfe56754a3d3d07233f9..9cd927601f518315c133f3830afb712fca00233e 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1,7 +1,6 @@ mod neovim_backed_binding_test_context; mod neovim_backed_test_context; mod neovim_connection; -mod vim_binding_test_context; mod vim_test_context; use std::sync::Arc; @@ -10,7 +9,6 @@ use command_palette::CommandPalette; use editor::DisplayPoint; pub use neovim_backed_binding_test_context::*; pub use neovim_backed_test_context::*; -pub use vim_binding_test_context::*; pub use vim_test_context::*; use indoc::indoc; diff --git a/crates/vim/src/test/vim_binding_test_context.rs b/crates/vim/src/test/vim_binding_test_context.rs deleted file mode 100644 index 04afe0c058d9983773add9dd4d5e853db60a7c5c..0000000000000000000000000000000000000000 --- a/crates/vim/src/test/vim_binding_test_context.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use crate::*; - -use super::VimTestContext; - -pub struct VimBindingTestContext<'a, const COUNT: usize> { - cx: VimTestContext<'a>, - keystrokes_under_test: [&'static str; COUNT], - mode_before: Mode, - mode_after: Mode, -} - -impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> { - pub fn new( - keystrokes_under_test: [&'static str; COUNT], - mode_before: Mode, - mode_after: Mode, - cx: VimTestContext<'a>, - ) -> Self { - Self { - cx, - keystrokes_under_test, - mode_before, - mode_after, - } - } - - pub fn binding( - self, - keystrokes_under_test: [&'static str; NEW_COUNT], - ) -> VimBindingTestContext<'a, NEW_COUNT> { - VimBindingTestContext { - keystrokes_under_test, - cx: self.cx, - mode_before: self.mode_before, - mode_after: self.mode_after, - } - } - - pub fn assert(&mut self, initial_state: &str, state_after: &str) { - self.cx.assert_binding( - self.keystrokes_under_test, - initial_state, - self.mode_before, - state_after, - self.mode_after, - ) - } -} - -impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> { - type Target = VimTestContext<'a>; - - fn deref(&self) -> &Self::Target { - &self.cx - } -} - -impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 24fb16fd3d736f4afec804ce8752c1c7e42f8232..9b0373957035213b6de019425f4484aaa647920a 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -8,8 +8,6 @@ use search::{BufferSearchBar, ProjectSearchBar}; use crate::{state::Operator, *}; -use super::VimBindingTestContext; - pub struct VimTestContext<'a> { cx: EditorLspTestContext<'a>, } @@ -126,14 +124,6 @@ impl<'a> VimTestContext<'a> { assert_eq!(self.mode(), mode_after, "{}", self.assertion_context()); assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); } - - pub fn binding( - mut self, - keystrokes: [&'static str; COUNT], - ) -> VimBindingTestContext<'a, COUNT> { - let mode = self.mode(); - VimBindingTestContext::new(keystrokes, mode, mode, self) - } } impl<'a> Deref for VimTestContext<'a> { diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index c8ca4df72bed76aaeb232d0b9dfc5d3293e5546f..4a96b5bbea165db11bcd42b69baa25e055342e50 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,5 +1,6 @@ use editor::{ClipboardSelection, Editor}; use gpui::{AppContext, ClipboardItem}; +use language::Point; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut AppContext) { let selections = editor.selections.all_adjusted(cx); @@ -9,7 +10,7 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App { let mut is_first = true; for selection in selections.iter() { - let start = selection.start; + let mut start = selection.start; let end = selection.end; if is_first { is_first = false; @@ -17,9 +18,25 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App text.push_str("\n"); } let initial_len = text.len(); + + // if the file does not end with \n, and our line-mode selection ends on + // that line, we will have expanded the start of the selection to ensure it + // contains a newline (so that delete works as expected). We undo that change + // here. + let is_last_line = linewise + && end.row == buffer.max_buffer_row() + && buffer.max_point().column > 0 + && start == Point::new(start.row, buffer.line_len(start.row)); + + if is_last_line { + start = Point::new(buffer.max_buffer_row(), 0); + } for chunk in buffer.text_for_range(start..end) { text.push_str(chunk); } + if is_last_line { + text.push_str("\n"); + } clipboard_selections.push(ClipboardSelection { len: text.len() - initial_len, is_entire_line: linewise, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1a11721a4e3c7cd9fb2456c00ac151387b55f57b..ea4847bd789ec38c35a0e33927793066443beaf7 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -563,38 +563,41 @@ mod test { #[gpui::test] async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["shift-v", "x"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" The quˇick brown fox jumps over the lazy dog"}) .await; - // Test pasting code copied on delete - cx.simulate_shared_keystroke("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", "x"]); - cx.assert(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; // Test pasting code copied on delete cx.simulate_shared_keystroke("p").await; cx.assert_state_matches().await; - cx.assert_all(indoc! {" + cx.set_shared_state(indoc! {" The quick brown - fox juˇmps over + fox jumps over the laˇzy dog"}) .await; + cx.simulate_shared_keystrokes(["shift-v", "x"]).await; + cx.assert_state_matches().await; + cx.assert_shared_clipboard("the lazy dog\n").await; + + for marked_text in cx.each_marked_position(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + { + cx.set_shared_state(&marked_text).await; + cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await; + cx.assert_state_matches().await; + // Test pasting code copied on delete + cx.simulate_shared_keystroke("p").await; + cx.assert_state_matches().await; + } cx.set_shared_state(indoc! {" The ˇlong line @@ -608,86 +611,57 @@ mod test { #[gpui::test] async fn test_visual_yank(cx: &mut gpui::TestAppContext) { - let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["v", "w", "y"]); - cx.assert("The quick ˇbrown", "The quick ˇbrown"); - cx.assert_clipboard_content(Some("brown")); - let mut cx = cx.binding(["v", "w", "j", "y"]); - cx.assert( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("The quick ˇbrown").await; + cx.simulate_shared_keystrokes(["v", "w", "y"]).await; + cx.assert_shared_state("The quick ˇbrown").await; + cx.assert_shared_clipboard("brown").await; + + cx.set_shared_state(indoc! {" The ˇquick brown fox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - quick brown - fox jumps o"})); - cx.assert( - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - ); - cx.assert_clipboard_content(Some("lazy d")); - cx.assert( - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - over - t"})); + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await; + cx.assert_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.assert_shared_clipboard(indoc! {" + quick brown + fox jumps o"}) + .await; + + cx.set_shared_state(indoc! {" + The quick brown + fox jumps over + the ˇlazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await; + cx.assert_shared_state(indoc! {" + The quick brown + fox jumps over + the ˇlazy dog"}) + .await; + cx.assert_shared_clipboard("lazy d").await; + cx.simulate_shared_keystrokes(["shift-v", "y"]).await; + cx.assert_shared_clipboard("the lazy dog\n").await; + let mut cx = cx.binding(["v", "b", "k", "y"]); - cx.assert( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}, - ); + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await; + cx.assert_shared_state(indoc! {" + ˇThe quick brown + fox jumps over + the lazy dog"}) + .await; cx.assert_clipboard_content(Some("The q")); - cx.assert( - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - fox jumps over - the l"})); - cx.assert( - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - quick brown - fox jumps o"})); } #[gpui::test] diff --git a/crates/vim/test_data/test_visual_line_delete.json b/crates/vim/test_data/test_visual_line_delete.json index 51406266f655984c98abfd70be6edc489c715930..e221a4ad5f0d63ac8f50f48ff62aa4a3807bb400 100644 --- a/crates/vim/test_data/test_visual_line_delete.json +++ b/crates/vim/test_data/test_visual_line_delete.json @@ -4,14 +4,11 @@ {"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}} {"Key":"p"} {"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} {"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} {"Key":"shift-v"} {"Key":"x"} {"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} {"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} {"Key":"shift-v"} {"Key":"j"} @@ -19,16 +16,6 @@ {"Get":{"state":"the laˇzy dog","mode":"Normal"}} {"Key":"p"} {"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The quˇick brown","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"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":"$"} diff --git a/crates/vim/test_data/test_visual_yank.json b/crates/vim/test_data/test_visual_yank.json new file mode 100644 index 0000000000000000000000000000000000000000..edc3b4f83d341052ae847ade2a75b9e2ec18d7a7 --- /dev/null +++ b/crates/vim/test_data/test_visual_yank.json @@ -0,0 +1,29 @@ +{"Put":{"state":"The quick ˇbrown"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"y"} +{"Get":{"state":"The quick ˇbrown","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"brown"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"j"} +{"Key":"y"} +{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"quick brown\nfox jumps o"}} +{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"j"} +{"Key":"y"} +{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"lazy d"}} +{"Key":"shift-v"} +{"Key":"y"} +{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"b"} +{"Key":"k"} +{"Key":"y"} +{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} From 3f9f742530cdb3f5684a22f4934cf3a949e74a58 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 11:45:52 +0200 Subject: [PATCH 034/142] update rate limiting embeddings strategy to delay less --- crates/semantic_index/src/embedding.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 77457ec7f6e34961ab2a784ef6f0d8068c4c1dbb..f2269a786a66af3896e0410c5558f3f18d618bd0 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -106,8 +106,8 @@ impl OpenAIEmbeddings { #[async_trait] impl EmbeddingProvider for OpenAIEmbeddings { async fn embed_batch(&self, spans: Vec<&str>) -> Result>> { - const BACKOFF_SECONDS: [usize; 3] = [45, 75, 125]; - const MAX_RETRIES: usize = 3; + const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45]; + const MAX_RETRIES: usize = 4; let api_key = OPENAI_API_KEY .as_ref() From aa07872a24bc25dfe97d4b460b3b7d919e4e9ae9 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 12:36:33 +0200 Subject: [PATCH 035/142] accomodate for duplicate entries in indexing queue Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 37 ++++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 0df3a9cc8428e37825c4e982525c3934e8efa8b3..65b7e405256c1697bd862665ec39d7dc5537e31c 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -12,6 +12,7 @@ use db::VectorDatabase; use embedding::{EmbeddingProvider, OpenAIEmbeddings}; use futures::{channel::oneshot, Future}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; +use isahc::http::header::OccupiedEntry; use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; @@ -130,10 +131,11 @@ impl ProjectState { let (job_queue_tx, job_queue_rx) = channel::unbounded(); let _queue_update_task = cx.background().spawn({ - let mut worktree_queue = Vec::new(); + let mut worktree_queue = HashMap::new(); async move { while let Ok(operation) = job_queue_rx.recv().await { Self::update_queue(&mut worktree_queue, operation); + dbg!(worktree_queue.len()); } } }); @@ -153,24 +155,37 @@ impl ProjectState { self.outstanding_job_count_rx.borrow().clone() } - fn update_queue(queue: &mut Vec, operation: IndexOperation) { + fn update_queue(queue: &mut HashMap, operation: IndexOperation) { match operation { IndexOperation::FlushQueue => { - while let Some(op) = queue.pop() { + let queue = std::mem::take(queue); + for (_, op) in queue { match op { - IndexOperation::IndexFile { payload, tx } => { + IndexOperation::IndexFile { + absolute_path, + payload, + tx, + } => { tx.try_send(payload); } - IndexOperation::DeleteFile { payload, tx } => { + IndexOperation::DeleteFile { + absolute_path, + payload, + tx, + } => { tx.try_send(payload); } _ => {} } } } - _ => { - // TODO: This has to accomodate for duplicate files to index. - queue.push(operation); + IndexOperation::IndexFile { + ref absolute_path, .. + } + | IndexOperation::DeleteFile { + ref absolute_path, .. + } => { + queue.insert(absolute_path.clone(), operation); } } } @@ -209,13 +224,14 @@ pub struct PendingFile { modified_time: SystemTime, job_handle: JobHandle, } - enum IndexOperation { IndexFile { + absolute_path: PathBuf, payload: PendingFile, tx: channel::Sender, }, DeleteFile { + absolute_path: PathBuf, payload: DbOperation, tx: channel::Sender, }, @@ -718,6 +734,7 @@ impl SemanticIndex { let job_handle = JobHandle::new(&outstanding_job_tx); let new_operation = IndexOperation::IndexFile { + absolute_path: absolute_path.clone(), payload: PendingFile { worktree_db_id, relative_path, @@ -733,6 +750,7 @@ impl SemanticIndex { } PathChange::Removed => { let new_operation = IndexOperation::DeleteFile { + absolute_path, payload: DbOperation::Delete { worktree_id: worktree_db_id, path: relative_path, @@ -853,6 +871,7 @@ impl SemanticIndex { if !already_stored { let job_handle = JobHandle::new(&job_count_tx); worktree_files.push(IndexOperation::IndexFile { + absolute_path: absolute_path.clone(), payload: PendingFile { worktree_db_id: db_ids_by_worktree_id[&worktree.id()], relative_path: path_buf, From afe0e74868f65ba16a80a35089f252af20b1bfc8 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 12:42:41 +0200 Subject: [PATCH 036/142] remove worktree_file_mtimes in state as it is no longer used Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 65b7e405256c1697bd862665ec39d7dc5537e31c..9d789d0eacad5eb2c8e5e51cfd7312b6f43d0b0c 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -94,7 +94,6 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, - worktree_file_mtimes: HashMap>, subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, @@ -122,7 +121,6 @@ impl ProjectState { cx: &mut AppContext, subscription: gpui::Subscription, worktree_db_ids: Vec<(WorktreeId, i64)>, - worktree_file_mtimes: HashMap>, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, ) -> Self { @@ -142,7 +140,6 @@ impl ProjectState { Self { worktree_db_ids, - worktree_file_mtimes, outstanding_job_count_rx, _outstanding_job_count_tx, subscription, @@ -834,7 +831,6 @@ impl SemanticIndex { let job_count_tx = Arc::new(Mutex::new(job_count_tx)); let job_count_tx_longlived = job_count_tx.clone(); - let worktree_file_mtimes_all = worktree_file_mtimes.clone(); let worktree_files = cx .background() .spawn(async move { @@ -896,7 +892,6 @@ impl SemanticIndex { cx, _subscription, worktree_db_ids, - worktree_file_mtimes_all, job_count_rx, job_count_tx_longlived, ); From e8e7b294d84b677256f1a10645ab65f7ecae135b Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 12:49:20 +0200 Subject: [PATCH 037/142] add delete files operation for remaining files in database not included in current worktree Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 9d789d0eacad5eb2c8e5e51cfd7312b6f43d0b0c..cca63c00aaf89aacaa4683c15739f90ebd65bba5 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -797,6 +797,7 @@ impl SemanticIndex { let language_registry = self.language_registry.clone(); let parsing_files_tx = self.parsing_files_tx.clone(); + let db_update_tx = self.db_update_tx.clone(); cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; @@ -837,6 +838,7 @@ impl SemanticIndex { let mut worktree_files = Vec::new(); for worktree in worktrees.into_iter() { let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); + let worktree_db_id = db_ids_by_worktree_id[&worktree.id()]; for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); @@ -869,7 +871,7 @@ impl SemanticIndex { worktree_files.push(IndexOperation::IndexFile { absolute_path: absolute_path.clone(), payload: PendingFile { - worktree_db_id: db_ids_by_worktree_id[&worktree.id()], + worktree_db_id, relative_path: path_buf, absolute_path, language, @@ -881,6 +883,17 @@ impl SemanticIndex { } } } + // Clean up entries from database that are no longer in the worktree. + for (path, mtime) in file_mtimes { + worktree_files.push(IndexOperation::DeleteFile { + absolute_path: worktree.absolutize(path.as_path()), + payload: DbOperation::Delete { + worktree_id: worktree_db_id, + path, + }, + tx: db_update_tx.clone(), + }); + } } anyhow::Ok(worktree_files) From a1519e4c38bff6879616f261fb95ff0fec7262b9 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 13:14:19 +0200 Subject: [PATCH 038/142] move semantic search project intialization to a subscribe event for workspace created Co-authored-by: Piotr --- crates/search/src/project_search.rs | 10 ---------- crates/semantic_index/src/semantic_index.rs | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 357aca2ed86690257bee89aa28d075ae79b62a5b..b0ec7219d138c03da94fcceaeb7f2ca682084e1e 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -844,16 +844,6 @@ impl ProjectSearchView { .detach(); let filters_enabled = false; - // Initialize Semantic Index if Needed - if SemanticIndex::enabled(cx) { - let model = model.read(cx); - project = model.project.clone(); - SemanticIndex::global(cx).map(|semantic| { - dbg!("Initializing project"); - semantic.update(cx, |this, cx| this.initialize_project(project.clone(), cx)); - }); - } - // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { search_id: model.read(cx).search_id, diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index cca63c00aaf89aacaa4683c15739f90ebd65bba5..fed320becdc3ef3bc0e7c5ce84632e0536283070 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -35,6 +35,7 @@ use util::{ paths::EMBEDDINGS_DIR, ResultExt, }; +use workspace::WorkspaceCreated; const SEMANTIC_INDEX_VERSION: usize = 7; const EMBEDDINGS_BATCH_SIZE: usize = 80; @@ -56,6 +57,22 @@ pub fn init( return; } + cx.subscribe_global::({ + move |event, mut cx| { + let Some(semantic_index) = SemanticIndex::global(cx) else { return; }; + let workspace = &event.0; + if let Some(workspace) = workspace.upgrade(cx) { + let project = workspace.read(cx).project().clone(); + if project.read(cx).is_local() { + semantic_index.update(cx, |index, cx| { + index.initialize_project(project, cx); + }); + } + } + } + }) + .detach(); + cx.spawn(move |mut cx| async move { let semantic_index = SemanticIndex::new( fs, @@ -133,7 +150,6 @@ impl ProjectState { async move { while let Ok(operation) = job_queue_rx.recv().await { Self::update_queue(&mut worktree_queue, operation); - dbg!(worktree_queue.len()); } } }); From 0b204bfdc8d083c05e6b0637b4f3dbdf0eff9cde Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 13:40:04 +0200 Subject: [PATCH 039/142] reindex semantic index when search project pane is reactivated in semantic mode Co-authored-by: Piotr --- crates/search/src/project_search.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b0ec7219d138c03da94fcceaeb7f2ca682084e1e..f665c4ddcdf585c4373406bbef12a01d88a2ceca 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1635,6 +1635,12 @@ impl ToolbarItemView for ProjectSearchBar { self.subscription = None; self.active_project_search = None; if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { + search.update(cx, |search, cx| { + if search.current_mode == SearchMode::Semantic { + search.index_project(cx); + } + }); + self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.active_project_search = Some(search); ToolbarItemLocation::PrimaryLeft { From a892a51ec30bf9b6dec54399615e835528dfdac8 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 13:46:43 +0200 Subject: [PATCH 040/142] update initialize project call to accomodate for test scenarios Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index fed320becdc3ef3bc0e7c5ce84632e0536283070..4f932b0622c7da8d4102f636f6bd833f08dff2a1 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -65,7 +65,7 @@ pub fn init( let project = workspace.read(cx).project().clone(); if project.read(cx).is_local() { semantic_index.update(cx, |index, cx| { - index.initialize_project(project, cx); + index.initialize_project(project, cx).detach_and_log_err(cx) }); } } @@ -785,7 +785,7 @@ impl SemanticIndex { &mut self, project: ModelHandle, cx: &mut ModelContext, - ) { + ) -> Task> { let worktree_scans_complete = project .read(cx) .worktrees(cx) @@ -931,10 +931,8 @@ impl SemanticIndex { this.projects.insert(project.downgrade(), project_state); }); - - cx.background().spawn(async move { anyhow::Ok(()) }).await + Result::<(), _>::Ok(()) }) - .detach_and_log_err(cx) } pub fn index_project( From 131950f6702c2af6549438673e5d79a00cface1d Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 18:40:08 +0200 Subject: [PATCH 041/142] add handling for Added file events to semantic index --- crates/semantic_index/src/semantic_index.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 4f932b0622c7da8d4102f636f6bd833f08dff2a1..474025c25e3d435d36518c6125396e78ac6aba55 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -728,9 +728,9 @@ impl SemanticIndex { continue; } + log::trace!("File Event: {:?}, Path: {:?}", &path_change, &path); match path_change { - PathChange::AddedOrUpdated | PathChange::Updated => { - log::trace!("File Updated: {:?}", path); + PathChange::AddedOrUpdated | PathChange::Updated | PathChange::Added => { if let Ok(language) = language_registry .language_for_file(&relative_path, None) .await @@ -786,6 +786,7 @@ impl SemanticIndex { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { + log::trace!("Initializing Project for Semantic Index"); let worktree_scans_complete = project .read(cx) .worktrees(cx) @@ -807,7 +808,7 @@ impl SemanticIndex { let _subscription = cx.subscribe(&project, |this, project, event, cx| { if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { - this.project_entries_changed(project, changes.clone(), cx, worktree_id); + this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id); }; }); From 199be8241c5ab47d7d41b0b36402ca3133e1944d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 24 Aug 2023 11:25:20 -0700 Subject: [PATCH 042/142] Add following into channel notes co-authored-by: max --- crates/collab_ui/src/channel_view.rs | 108 ++++++++++++++++++++++++++- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/items.rs | 3 +- crates/rpc/proto/zed.proto | 5 ++ crates/workspace/src/item.rs | 2 +- crates/workspace/src/workspace.rs | 23 +++--- 6 files changed, 128 insertions(+), 17 deletions(-) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index af45eabe690fd678867be093c58c6bedd86c0271..c13711b29c1db1d0633bee994374fb5edd2ee72e 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,27 +1,34 @@ use channel::channel_buffer::ChannelBuffer; +use client::proto; use clock::ReplicaId; use collections::HashMap; use editor::Editor; use gpui::{ actions, elements::{ChildView, Label}, - AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, + AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, View, + ViewContext, ViewHandle, }; use language::Language; use project::Project; use std::sync::Arc; -use workspace::item::{Item, ItemHandle}; +use workspace::{ + item::{FollowableItem, Item, ItemHandle}, + register_followable_item, ViewId, +}; actions!(channel_view, [Deploy]); pub(crate) fn init(cx: &mut AppContext) { - // TODO + register_followable_item::(cx) } pub struct ChannelView { editor: ViewHandle, project: ModelHandle, channel_buffer: ModelHandle, + remote_id: Option, + _editor_event_subscription: Subscription, } impl ChannelView { @@ -34,14 +41,19 @@ impl ChannelView { let buffer = channel_buffer.read(cx).buffer(); buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + let this = Self { editor, project, channel_buffer, + remote_id: None, + _editor_event_subscription, }; let mapping = this.project_replica_ids_by_channel_buffer_replica_id(cx); this.editor .update(cx, |editor, cx| editor.set_replica_id_mapping(mapping, cx)); + this } @@ -82,9 +94,15 @@ impl View for ChannelView { "ChannelView" } - fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement { + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { ChildView::new(self.editor.as_any(), cx).into_any() } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(self.editor.as_any()) + } + } } impl Item for ChannelView { @@ -104,3 +122,85 @@ impl Item for ChannelView { Label::new(channel_name, style.label.to_owned()).into_any() } } + +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + self.channel_buffer.read(cx).channel(cx).map(|channel| { + proto::view::Variant::ChannelView(proto::view::ChannelView { + channel_id: channel.id, + }) + }) + } + + fn from_state_proto( + _: ViewHandle, + workspace: ViewHandle, + remote_id: workspace::ViewId, + state_proto: &mut Option, + cx: &mut AppContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state_proto else { return None }; + let Some(proto::view::Variant::ChannelView(state)) = state_proto.take() else { unreachable!() }; + + let channel_store = &workspace.read(cx).app_state().channel_store.clone(); + let open_channel_buffer = channel_store.update(cx, |store, cx| { + store.open_channel_buffer(state.channel_id, cx) + }); + let project = workspace.read(cx).project().to_owned(); + let language = workspace.read(cx).app_state().languages.clone(); + let get_markdown = language.language_for_name("Markdown"); + + Some(cx.spawn(|mut cx| async move { + let channel_buffer = open_channel_buffer.await?; + let markdown = get_markdown.await?; + + let this = workspace + .update(&mut cx, move |_, cx| { + cx.add_view(|cx| { + let mut this = Self::new(project, channel_buffer, Some(markdown), cx); + this.remote_id = Some(remote_id); + this + }) + }) + .ok_or_else(|| anyhow::anyhow!("workspace droppped"))?; + + Ok(this) + })) + } + + fn add_event_to_update_proto( + &self, + _: &Self::Event, + _: &mut Option, + _: &AppContext, + ) -> bool { + false + } + + fn apply_update_proto( + &mut self, + _: &ModelHandle, + _: proto::update_view::Variant, + _: &mut ViewContext, + ) -> gpui::Task> { + gpui::Task::ready(Ok(())) + } + + fn set_leader_replica_id( + &mut self, + leader_replica_id: Option, + cx: &mut ViewContext, + ) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_replica_id(leader_replica_id, cx) + }) + } + + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { + Editor::should_unfollow_on_event(event, cx) + } +} diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e031edf538db18be53b79af0eb80ec820f499fa0..a2a561402f904be26d7a3a8d316519fb60387a19 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6384,7 +6384,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - project.clone(), + workspace.clone(), ViewId { creator: Default::default(), id: 0, @@ -6479,7 +6479,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - project.clone(), + workspace.clone(), ViewId { creator: Default::default(), id: 0, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 668ea482035ed5731cc94e8b2e6c5dbe63e5919f..657aae5ff9df1c4707ba42d3d188e3f9fa391a5e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -49,11 +49,12 @@ impl FollowableItem for Editor { fn from_state_proto( pane: ViewHandle, - project: ModelHandle, + workspace: ViewHandle, remote_id: ViewId, state: &mut Option, cx: &mut AppContext, ) -> Option>>> { + let project = workspace.read(cx).project().to_owned(); let Some(proto::view::Variant::Editor(_)) = state else { return None }; let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index b97feff06ba6ab72db602cbd5b307778501c2a19..827468b28012a262eddd3700930971d38a3fd798 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1120,6 +1120,7 @@ message View { oneof variant { Editor editor = 3; + ChannelView channel_view = 4; } message Editor { @@ -1132,6 +1133,10 @@ message View { float scroll_x = 7; float scroll_y = 8; } + + message ChannelView { + uint64 channel_id = 1; + } } message Collaborator { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a115e0f4735f5581cce108fa2fa63d498d8b11f5..4b5b7a793118981529c2b7bfb3d0bbaa94121660 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -674,7 +674,7 @@ pub trait FollowableItem: Item { fn to_state_proto(&self, cx: &AppContext) -> Option; fn from_state_proto( pane: ViewHandle, - project: ModelHandle, + project: ViewHandle, id: ViewId, state: &mut Option, cx: &mut AppContext, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a8354472aa5a26385c6919002a830aa225cbce46..62bb7a82a29619d7f6bec11053db56cbf75a5faf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -345,7 +345,7 @@ pub fn register_project_item(cx: &mut AppContext) { type FollowableItemBuilder = fn( ViewHandle, - ModelHandle, + ViewHandle, ViewId, &mut Option, &mut AppContext, @@ -362,8 +362,8 @@ pub fn register_followable_item(cx: &mut AppContext) { builders.insert( TypeId::of::(), ( - |pane, project, id, state, cx| { - I::from_state_proto(pane, project, id, state, cx).map(|task| { + |pane, workspace, id, state, cx| { + I::from_state_proto(pane, workspace, id, state, cx).map(|task| { cx.foreground() .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) }) @@ -2848,7 +2848,13 @@ impl Workspace { views: Vec, cx: &mut AsyncAppContext, ) -> Result<()> { - let project = this.read_with(cx, |this, _| this.project.clone())?; + let this = this + .upgrade(cx) + .ok_or_else(|| anyhow!("workspace dropped"))?; + let project = this + .read_with(cx, |this, _| this.project.clone()) + .ok_or_else(|| anyhow!("window dropped"))?; + let replica_id = project .read_with(cx, |project, _| { project @@ -2874,12 +2880,11 @@ impl Workspace { let id = ViewId::from_proto(id.clone())?; let mut variant = view.variant.clone(); if variant.is_none() { - Err(anyhow!("missing variant"))?; + Err(anyhow!("missing view variant"))?; } for build_item in &item_builders { - let task = cx.update(|cx| { - build_item(pane.clone(), project.clone(), id, &mut variant, cx) - }); + let task = cx + .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx)); if let Some(task) = task { item_tasks.push(task); leader_view_ids.push(id); @@ -2907,7 +2912,7 @@ impl Workspace { } Some(()) - })?; + }); } Ok(()) } From 3268cce41a948ea422f0fe0389cbd8d2e260c047 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Aug 2023 11:30:43 -0700 Subject: [PATCH 043/142] Fix error in update_channel_buffer when there are no operations to store Co-authored-by: Mikayla --- crates/collab/src/db/queries/buffers.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 83f5b87079416259f673f1ddeedad4e31e72aa21..a38693bace24364e00ab54bed79c68a95a30f9f0 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -215,7 +215,7 @@ impl Database { user: UserId, operations: &[proto::Operation], ) -> Result> { - self.transaction(|tx| async move { + self.transaction(move |tx| async move { self.check_user_is_channel_member(channel_id, user, &*tx) .await?; @@ -240,11 +240,15 @@ impl Database { .await? .ok_or_else(|| anyhow!("missing buffer snapshot"))?; - buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { - operation_to_storage(operation, &buffer, serialization_version) - })) - .exec(&*tx) - .await?; + let operations = operations + .iter() + .filter_map(|op| operation_to_storage(op, &buffer, serialization_version)) + .collect::>(); + if !operations.is_empty() { + buffer_operation::Entity::insert_many(operations) + .exec(&*tx) + .await?; + } let mut connections = Vec::new(); let mut rows = channel_buffer_collaborator::Entity::find() From 24141c2f16261c25ebedfa01414ce4b2a62055c0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Aug 2023 13:32:16 -0700 Subject: [PATCH 044/142] Ensure collaborators cursor colors are the same in channel buffers as in projects Co-authored-by: Mikayla --- crates/channel/src/channel_buffer.rs | 13 ++- .../collab/src/tests/channel_buffer_tests.rs | 59 ++++++++++-- crates/collab_ui/src/channel_view.rs | 94 +++++++++++++------ crates/editor/src/editor.rs | 10 +- crates/editor/src/element.rs | 50 ++++++---- crates/language/src/buffer.rs | 8 ++ crates/project/src/project.rs | 2 + crates/sum_tree/src/tree_map.rs | 12 ++- crates/theme/src/theme.rs | 1 + styles/src/style_tree/editor.ts | 1 + 10 files changed, 189 insertions(+), 61 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 5ee3fd6c849465f0d0f3288317b5798dd0e683a8..cad3c4f58f83fa460703a881fb1ee48493f5390d 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -21,8 +21,12 @@ pub struct ChannelBuffer { _subscription: client::Subscription, } +pub enum Event { + CollaboratorsChanged, +} + impl Entity for ChannelBuffer { - type Event = (); + type Event = Event; fn release(&mut self, _: &mut AppContext) { self.client @@ -54,8 +58,9 @@ impl ChannelBuffer { let collaborators = response.collaborators; - let buffer = - cx.add_model(|cx| language::Buffer::new(response.replica_id as u16, base_text, cx)); + let buffer = cx.add_model(|_| { + language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) + }); buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; let subscription = client.subscribe_to_entity(channel_id)?; @@ -111,6 +116,7 @@ impl ChannelBuffer { this.update(&mut cx, |this, cx| { this.collaborators.push(collaborator); + cx.emit(Event::CollaboratorsChanged); cx.notify(); }); @@ -134,6 +140,7 @@ impl ChannelBuffer { true } }); + cx.emit(Event::CollaboratorsChanged); cx.notify(); }); diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 8fb50055f5a46c0409b4ed94d0a5653da02579d2..6a9ef3fc13533d9f109b22b04fd642395354079f 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -63,6 +63,10 @@ async fn test_core_channel_buffers( // Client B sees the correct text, and then edits it let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), + buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) + ); assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); buffer_b.update(cx_b, |buffer, cx| { buffer.edit([(7..12, "beautiful")], None, cx) @@ -138,6 +142,7 @@ async fn test_channel_buffer_replica_ids( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); // Clients A and B join a channel. active_call_a @@ -190,7 +195,7 @@ async fn test_channel_buffer_replica_ids( // Client C is in a separate project. client_c.fs().insert_tree("/dir", json!({})).await; - let (project_c, _) = client_c.build_local_project("/dir", cx_c).await; + let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await; // Note that each user has a different replica id in the projects vs the // channel buffer. @@ -211,8 +216,14 @@ async fn test_channel_buffer_replica_ids( .add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), None, cx)); let channel_window_b = cx_b .add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), None, cx)); - let channel_window_c = cx_c - .add_window(|cx| ChannelView::new(project_c.clone(), channel_buffer_c.clone(), None, cx)); + let channel_window_c = cx_c.add_window(|cx| { + ChannelView::new( + separate_project_c.clone(), + channel_buffer_c.clone(), + None, + cx, + ) + }); let channel_view_a = channel_window_a.root(cx_a); let channel_view_b = channel_window_b.root(cx_b); @@ -222,24 +233,54 @@ async fn test_channel_buffer_replica_ids( // so that they match the same users' replica ids in their shared project. channel_view_a.read_with(cx_a, |view, cx| { assert_eq!( - view.project_replica_ids_by_channel_buffer_replica_id(cx), - [(1, 0), (2, 1)].into_iter().collect::>() + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1)].into_iter().collect::>() ); }); channel_view_b.read_with(cx_b, |view, cx| { assert_eq!( - view.project_replica_ids_by_channel_buffer_replica_id(cx), - [(1, 0), (2, 1)].into_iter().collect::>(), + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1)].into_iter().collect::>(), ) }); // Client C only sees themself, as they're not part of any shared project channel_view_c.read_with(cx_c, |view, cx| { assert_eq!( - view.project_replica_ids_by_channel_buffer_replica_id(cx), - [(0, 0)].into_iter().collect::>(), + view.editor.read(cx).replica_id_map().unwrap(), + &[(0, 0)].into_iter().collect::>(), + ); + }); + + // Client C joins the project that clients A and B are in. + active_call_c + .update(cx_c, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + let project_c = client_c.build_remote_project(shared_project_id, cx_c).await; + deterministic.run_until_parked(); + project_c.read_with(cx_c, |project, _| { + assert_eq!(project.replica_id(), 2); + }); + + // For clients A and B, client C's replica id in the channel buffer is + // now mapped to their replica id in the shared project. + channel_view_a.read_with(cx_a, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1), (0, 2)] + .into_iter() + .collect::>() ); }); + channel_view_b.read_with(cx_b, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1), (0, 2)] + .into_iter() + .collect::>(), + ) + }); } #[track_caller] diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index c13711b29c1db1d0633bee994374fb5edd2ee72e..dd3969d351f0fd1aaecba6dc84ee61b752347a5a 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,4 +1,4 @@ -use channel::channel_buffer::ChannelBuffer; +use channel::channel_buffer::{self, ChannelBuffer}; use client::proto; use clock::ReplicaId; use collections::HashMap; @@ -24,7 +24,7 @@ pub(crate) fn init(cx: &mut AppContext) { } pub struct ChannelView { - editor: ViewHandle, + pub editor: ViewHandle, project: ModelHandle, channel_buffer: ModelHandle, remote_id: Option, @@ -43,6 +43,10 @@ impl ChannelView { let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + cx.subscribe(&project, Self::handle_project_event).detach(); + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); + let this = Self { editor, project, @@ -50,38 +54,70 @@ impl ChannelView { remote_id: None, _editor_event_subscription, }; - let mapping = this.project_replica_ids_by_channel_buffer_replica_id(cx); - this.editor - .update(cx, |editor, cx| editor.set_replica_id_mapping(mapping, cx)); - + this.refresh_replica_id_map(cx); this } - /// Channel Buffer Replica ID -> Project Replica ID - pub fn project_replica_ids_by_channel_buffer_replica_id( - &self, - cx: &AppContext, - ) -> HashMap { + fn handle_project_event( + &mut self, + _: ModelHandle, + event: &project::Event, + cx: &mut ViewContext, + ) { + match event { + project::Event::RemoteIdChanged(_) => {} + project::Event::DisconnectedFromHost => {} + project::Event::Closed => {} + project::Event::CollaboratorUpdated { .. } => {} + project::Event::CollaboratorLeft(_) => {} + project::Event::CollaboratorJoined(_) => {} + _ => return, + } + self.refresh_replica_id_map(cx); + } + + fn handle_channel_buffer_event( + &mut self, + _: ModelHandle, + _: &channel_buffer::Event, + cx: &mut ViewContext, + ) { + self.refresh_replica_id_map(cx); + } + + /// Build a mapping of channel buffer replica ids to the corresponding + /// replica ids in the current project. + /// + /// Using this mapping, a given user can be displayed with the same color + /// in the channel buffer as in other files in the project. Users who are + /// in the channel buffer but not the project will not have a color. + fn refresh_replica_id_map(&self, cx: &mut ViewContext) { + let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default(); let project = self.project.read(cx); - let mut result = HashMap::default(); - result.insert( - self.channel_buffer.read(cx).replica_id(cx), - project.replica_id(), + let channel_buffer = self.channel_buffer.read(cx); + project_replica_ids_by_channel_buffer_replica_id + .insert(channel_buffer.replica_id(cx), project.replica_id()); + project_replica_ids_by_channel_buffer_replica_id.extend( + channel_buffer + .collaborators() + .iter() + .filter_map(|channel_buffer_collaborator| { + project + .collaborators() + .values() + .find_map(|project_collaborator| { + (project_collaborator.user_id == channel_buffer_collaborator.user_id) + .then_some(( + channel_buffer_collaborator.replica_id as ReplicaId, + project_collaborator.replica_id, + )) + }) + }), ); - for collaborator in self.channel_buffer.read(cx).collaborators() { - let project_replica_id = - project - .collaborators() - .values() - .find_map(|project_collaborator| { - (project_collaborator.user_id == collaborator.user_id) - .then_some(project_collaborator.replica_id) - }); - if let Some(project_replica_id) = project_replica_id { - result.insert(collaborator.replica_id as ReplicaId, project_replica_id); - } - } - result + + self.editor.update(cx, |editor, cx| { + editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx) + }); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e7197d98c5cacc3d1eb7f47a8c968d239182cb7e..775f3c07ece735c781cd60f9600bf7027320d25d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1606,12 +1606,16 @@ impl Editor { self.read_only = read_only; } - pub fn set_replica_id_mapping( + pub fn replica_id_map(&self) -> Option<&HashMap> { + self.replica_id_mapping.as_ref() + } + + pub fn set_replica_id_map( &mut self, - mapping: HashMap, + mapping: Option>, cx: &mut ViewContext, ) { - self.replica_id_mapping = Some(mapping); + self.replica_id_mapping = mapping; cx.notify(); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0f26e5819ca48e9d68988200f778fa361ddd9590..9f74eed790fa6b025bdd12125858ad02ec44d8ac 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -62,6 +62,7 @@ struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, is_newest: bool, + is_local: bool, range: Range, active_rows: Range, } @@ -73,6 +74,7 @@ impl SelectionLayout { cursor_shape: CursorShape, map: &DisplaySnapshot, is_newest: bool, + is_local: 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)); @@ -109,6 +111,7 @@ impl SelectionLayout { head, cursor_shape, is_newest, + is_local, range, active_rows, } @@ -763,7 +766,6 @@ impl EditorElement { cx: &mut PaintContext, ) { let style = &self.style; - let local_replica_id = editor.replica_id(cx); let scroll_position = layout.position_map.snapshot.scroll_position(); let start_row = layout.visible_display_row_range.start; let scroll_top = scroll_position.y() * layout.position_map.line_height; @@ -852,15 +854,13 @@ impl EditorElement { for (replica_id, selections) in &layout.selections { let replica_id = *replica_id; - let selection_style = style.replica_selection_style(replica_id); + let selection_style = if let Some(replica_id) = replica_id { + style.replica_selection_style(replica_id) + } else { + &style.absent_selection + }; for selection in selections { - if !selection.range.is_empty() - && (replica_id == local_replica_id - || Some(replica_id) == editor.leader_replica_id) - { - invisible_display_ranges.push(selection.range.clone()); - } self.paint_highlighted_range( scene, selection.range.clone(), @@ -874,7 +874,10 @@ impl EditorElement { bounds, ); - if editor.show_local_cursors(cx) || replica_id != local_replica_id { + if selection.is_local && !selection.range.is_empty() { + invisible_display_ranges.push(selection.range.clone()); + } + if !selection.is_local || editor.show_local_cursors(cx) { let cursor_position = selection.head; if layout .visible_display_row_range @@ -2124,7 +2127,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections: Vec<(ReplicaId, Vec)> = Vec::new(); + let mut selections: Vec<(Option, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut fold_ranges = Vec::new(); let is_singleton = editor.is_singleton(cx); @@ -2155,8 +2158,14 @@ impl Element for EditorElement { .buffer_snapshot .remote_selections_in_range(&(start_anchor..end_anchor)) { + let replica_id = if let Some(mapping) = &editor.replica_id_mapping { + mapping.get(&replica_id).copied() + } else { + None + }; + // The local selections match the leader's selections. - if Some(replica_id) == editor.leader_replica_id { + if replica_id.is_some() && replica_id == editor.leader_replica_id { continue; } remote_selections @@ -2168,6 +2177,7 @@ impl Element for EditorElement { cursor_shape, &snapshot.display_snapshot, false, + false, )); } selections.extend(remote_selections); @@ -2191,6 +2201,7 @@ impl Element for EditorElement { editor.cursor_shape, &snapshot.display_snapshot, is_newest, + true, ); if is_newest { newest_selection_head = Some(layout.head); @@ -2206,11 +2217,18 @@ impl Element for EditorElement { } // Render the local selections in the leader's color when following. - let local_replica_id = editor - .leader_replica_id - .unwrap_or_else(|| editor.replica_id(cx)); + let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id { + leader_replica_id + } else { + let replica_id = editor.replica_id(cx); + if let Some(mapping) = &editor.replica_id_mapping { + mapping.get(&replica_id).copied().unwrap_or(replica_id) + } else { + replica_id + } + }; - selections.push((local_replica_id, layouts)); + selections.push((Some(local_replica_id), layouts)); } let scrollbar_settings = &settings::get::(cx).scrollbar; @@ -2591,7 +2609,7 @@ pub struct LayoutState { blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, fold_ranges: Vec<(BufferRow, Range, Color)>, - selections: Vec<(ReplicaId, Vec)>, + selections: Vec<(Option, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, is_singleton: bool, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d032e8e0253c81681f3c9c11576185f5f04ce5e9..1b83ca59646cbe9f434aabed1c4f12b48778ca66 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -359,6 +359,14 @@ impl Buffer { ) } + pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self { + Self::build( + TextBuffer::new(replica_id, remote_id, base_text), + None, + None, + ) + } + pub fn from_proto( replica_id: ReplicaId, message: proto::BufferState, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bc4fa587cb83974a3ca16fff38c8e16e9ca169ff..49074268f21a11cc3b57b43ae2e4409c749df6d2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -282,6 +282,7 @@ pub enum Event { old_peer_id: proto::PeerId, new_peer_id: proto::PeerId, }, + CollaboratorJoined(proto::PeerId), CollaboratorLeft(proto::PeerId), RefreshInlayHints, } @@ -5931,6 +5932,7 @@ impl Project { let collaborator = Collaborator::from_proto(collaborator)?; this.update(&mut cx, |this, cx| { this.shared_buffers.remove(&collaborator.peer_id); + cx.emit(Event::CollaboratorJoined(collaborator.peer_id)); this.collaborators .insert(collaborator.peer_id, collaborator); cx.notify(); diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 4bb98d2ac8668cb96afbcf31c717f4bb87dbe16f..edb9010e50eb172f379071ad4b2139991c5650d4 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,7 +2,7 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct TreeMap(SumTree>) where K: Clone + Debug + Default + Ord, @@ -162,6 +162,16 @@ impl TreeMap { } } +impl Debug for TreeMap +where + K: Clone + Debug + Default + Ord, + V: Clone + Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + #[derive(Debug)] struct MapSeekTargetAdaptor<'a, T>(&'a T); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 0f34963708deae3797b88a2217f29860b2eeb74a..9005fc9757a97f1088ec19b12521389f81be9f73 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -756,6 +756,7 @@ pub struct Editor { pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, + pub absent_selection: SelectionStyle, pub syntax: Arc, pub hint: HighlightStyle, pub suggestion: HighlightStyle, diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 9ad008f38d4dd928af5b49c46b148df575b1c6a3..9277a2e7a1966c7d4eeac6cc7d3129bd52244141 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -184,6 +184,7 @@ export default function editor(): any { theme.players[6], theme.players[7], ], + absent_selection: theme.players[7], autocomplete: { background: background(theme.middle), corner_radius: 8, From 90f22cb0d24abb5d73dc6801990e5ce741e6a18d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 12:36:01 -0700 Subject: [PATCH 045/142] Replicate editor state when following into channel notes Co-authored-by: Mikayla --- crates/collab_ui/src/channel_view.rs | 57 ++++++++++++++++++++++------ crates/rpc/proto/zed.proto | 1 + 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index dd3969d351f0fd1aaecba6dc84ee61b752347a5a..be186fc2e2ad4f194925a9e94e87830fb1681550 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -168,6 +168,13 @@ impl FollowableItem for ChannelView { self.channel_buffer.read(cx).channel(cx).map(|channel| { proto::view::Variant::ChannelView(proto::view::ChannelView { channel_id: channel.id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, }) }) } @@ -176,11 +183,11 @@ impl FollowableItem for ChannelView { _: ViewHandle, workspace: ViewHandle, remote_id: workspace::ViewId, - state_proto: &mut Option, + state: &mut Option, cx: &mut AppContext, ) -> Option>>> { - let Some(proto::view::Variant::ChannelView(_)) = state_proto else { return None }; - let Some(proto::view::Variant::ChannelView(state)) = state_proto.take() else { unreachable!() }; + let Some(proto::view::Variant::ChannelView(_)) = state else { return None }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() }; let channel_store = &workspace.read(cx).app_state().channel_store.clone(); let open_channel_buffer = channel_store.update(cx, |store, cx| { @@ -202,7 +209,29 @@ impl FollowableItem for ChannelView { this }) }) - .ok_or_else(|| anyhow::anyhow!("workspace droppped"))?; + .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?; + + if let Some(state) = state.editor { + let task = this.update(&mut cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + }) + }); + if let Some(task) = task { + task.await?; + } + } Ok(this) })) @@ -210,20 +239,24 @@ impl FollowableItem for ChannelView { fn add_event_to_update_proto( &self, - _: &Self::Event, - _: &mut Option, - _: &AppContext, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, ) -> bool { - false + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) } fn apply_update_proto( &mut self, - _: &ModelHandle, - _: proto::update_view::Variant, - _: &mut ViewContext, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut ViewContext, ) -> gpui::Task> { - gpui::Task::ready(Ok(())) + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) } fn set_leader_replica_id( diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 827468b28012a262eddd3700930971d38a3fd798..f032ccce513de5feacae35e9980acb18c864d6c8 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1136,6 +1136,7 @@ message View { message ChannelView { uint64 channel_id = 1; + Editor editor = 2; } } From 5888e7b214685aa1d8dd24e657d84c7b015aa08f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 13:40:44 -0700 Subject: [PATCH 046/142] Dedup channel buffers --- crates/channel/src/channel_buffer.rs | 68 ++++++++-------- crates/channel/src/channel_store.rs | 81 +++++++++++++++---- .../collab/src/tests/channel_buffer_tests.rs | 56 +++++++++++++ 3 files changed, 155 insertions(+), 50 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index cad3c4f58f83fa460703a881fb1ee48493f5390d..c19899501a43a360d1a4b2140c60d09f4a2fc608 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,7 +1,7 @@ use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; use client::Client; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; @@ -38,46 +38,44 @@ impl Entity for ChannelBuffer { } impl ChannelBuffer { - pub(crate) fn new( + pub(crate) async fn new( channel_store: ModelHandle, channel_id: ChannelId, client: Arc, - cx: &mut AppContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let response = client - .request(proto::JoinChannelBuffer { channel_id }) - .await?; - - let base_text = response.base_text; - let operations = response - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::, _>>()?; - - let collaborators = response.collaborators; - - let buffer = cx.add_model(|_| { - language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) - }); - buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + mut cx: AsyncAppContext, + ) -> Result> { + let response = client + .request(proto::JoinChannelBuffer { channel_id }) + .await?; - let subscription = client.subscribe_to_entity(channel_id)?; + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; - anyhow::Ok(cx.add_model(|cx| { - cx.subscribe(&buffer, Self::on_buffer_update).detach(); + let collaborators = response.collaborators; - Self { - buffer, - client, - channel_id, - channel_store, - collaborators, - _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), - } - })) - }) + let buffer = cx.add_model(|_| { + language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) + }); + buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + + let subscription = client.subscribe_to_entity(channel_id)?; + + anyhow::Ok(cx.add_model(|cx| { + cx.subscribe(&buffer, Self::on_buffer_update).detach(); + + Self { + buffer, + client, + channel_id, + channel_store, + collaborators, + _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), + } + })) } async fn handle_update_channel_buffer( diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a6aad19d03ac45439ca7ed5ab3dc6421b104b254..1d83bd1d7f2771d87fec37091e5ade1698d87c86 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,20 +1,13 @@ -use anyhow::anyhow; -use anyhow::Result; -use client::Status; -use client::UserId; -use client::{Client, Subscription, User, UserStore}; -use collections::HashMap; -use collections::HashSet; -use futures::channel::mpsc; -use futures::Future; -use futures::StreamExt; -use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use crate::channel_buffer::ChannelBuffer; +use anyhow::{anyhow, Result}; +use client::{Client, Status, Subscription, User, UserId, UserStore}; +use collections::{hash_map, HashMap, HashSet}; +use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; +use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; -use crate::channel_buffer::ChannelBuffer; - pub type ChannelId = u64; pub struct ChannelStore { @@ -25,6 +18,7 @@ pub struct ChannelStore { channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, + opened_buffers: HashMap, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -59,6 +53,11 @@ pub enum ChannelMemberStatus { NotMember, } +enum OpenedChannelBuffer { + Open(WeakModelHandle), + Loading(Shared, Arc>>>), +} + impl ChannelStore { pub fn new( client: Arc, @@ -89,6 +88,7 @@ impl ChannelStore { } } }); + Self { channels_by_id: HashMap::default(), channel_invitations: Vec::default(), @@ -96,6 +96,7 @@ impl ChannelStore { channel_participants: Default::default(), channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), + opened_buffers: Default::default(), update_channels_tx, client, user_store, @@ -154,11 +155,61 @@ impl ChannelStore { } pub fn open_channel_buffer( - &self, + &mut self, channel_id: ChannelId, cx: &mut ModelContext, ) -> Task>> { - ChannelBuffer::new(cx.handle(), channel_id, self.client.clone(), cx) + // Make sure that a given channel buffer is only opened once per + // app instance, even if this method is called multiple times + // with the same channel id while the first task is still running. + let task = loop { + match self.opened_buffers.entry(channel_id) { + hash_map::Entry::Occupied(e) => match e.get() { + OpenedChannelBuffer::Open(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + break Task::ready(Ok(buffer)).shared(); + } else { + self.opened_buffers.remove(&channel_id); + continue; + } + } + OpenedChannelBuffer::Loading(task) => break task.clone(), + }, + hash_map::Entry::Vacant(e) => { + let task = cx + .spawn(|this, cx| { + ChannelBuffer::new(this, channel_id, self.client.clone(), cx) + .map_err(Arc::new) + }) + .shared(); + e.insert(OpenedChannelBuffer::Loading(task.clone())); + cx.spawn({ + let task = task.clone(); + |this, mut cx| async move { + let result = task.await; + this.update(&mut cx, |this, cx| { + if let Ok(buffer) = result { + cx.observe_release(&buffer, move |this, _, _| { + this.opened_buffers.remove(&channel_id); + }) + .detach(); + this.opened_buffers.insert( + channel_id, + OpenedChannelBuffer::Open(buffer.downgrade()), + ); + } else { + this.opened_buffers.remove(&channel_id); + } + }); + } + }) + .detach(); + break task; + } + } + }; + cx.foreground() + .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) } pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 6a9ef3fc13533d9f109b22b04fd642395354079f..f7e5751a3791addaf1f76543cf77e65f05710277 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -3,6 +3,7 @@ use call::ActiveCall; use client::UserId; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use futures::future; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; use serde_json::json; @@ -283,6 +284,61 @@ async fn test_channel_buffer_replica_ids( }); } +#[gpui::test] +async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mut TestAppContext) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + + let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await; + + let channel_buffer_1 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + let channel_buffer_2 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + let channel_buffer_3 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + + // All concurrent tasks for opening a channel buffer return the same model handle. + let (channel_buffer_1, channel_buffer_2, channel_buffer_3) = + future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) + .await + .unwrap(); + let model_id = channel_buffer_1.id(); + assert_eq!(channel_buffer_1, channel_buffer_2); + assert_eq!(channel_buffer_1, channel_buffer_3); + + channel_buffer_1.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "hello")], None, cx); + }) + }); + deterministic.run_until_parked(); + + cx_a.update(|_| { + drop(channel_buffer_1); + drop(channel_buffer_2); + drop(channel_buffer_3); + }); + deterministic.run_until_parked(); + + // The channel buffer can be reopened after dropping it. + let channel_buffer = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .await + .unwrap(); + assert_ne!(channel_buffer.id(), model_id); + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, _| { + assert_eq!(buffer.text(), "hello"); + }) + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( From 1ae54ca62099b5eb9f762ca1f70bc5b179880481 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 14:29:04 -0700 Subject: [PATCH 047/142] Dedup channel views Co-authored-by: Mikayla --- .../collab/src/tests/channel_buffer_tests.rs | 15 +-- crates/collab_ui/src/channel_view.rs | 127 +++++++++++------- crates/collab_ui/src/collab_panel.rs | 42 ++---- 3 files changed, 96 insertions(+), 88 deletions(-) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index f7e5751a3791addaf1f76543cf77e65f05710277..0ecd4588c5f640b22490de7b0d101a04cc66aac4 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -213,17 +213,12 @@ async fn test_channel_buffer_replica_ids( assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0); }); - let channel_window_a = cx_a - .add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), None, cx)); - let channel_window_b = cx_b - .add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), None, cx)); + let channel_window_a = + cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx)); + let channel_window_b = + cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx)); let channel_window_c = cx_c.add_window(|cx| { - ChannelView::new( - separate_project_c.clone(), - channel_buffer_c.clone(), - None, - cx, - ) + ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx) }); let channel_view_a = channel_window_a.root(cx_a); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index be186fc2e2ad4f194925a9e94e87830fb1681550..0e2d3636aa281c23ec800788140c37351faba563 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,4 +1,8 @@ -use channel::channel_buffer::{self, ChannelBuffer}; +use anyhow::{anyhow, Result}; +use channel::{ + channel_buffer::{self, ChannelBuffer}, + ChannelId, +}; use client::proto; use clock::ReplicaId; use collections::HashMap; @@ -6,15 +10,13 @@ use editor::Editor; use gpui::{ actions, elements::{ChildView, Label}, - AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, View, + AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, }; -use language::Language; use project::Project; -use std::sync::Arc; use workspace::{ item::{FollowableItem, Item, ItemHandle}, - register_followable_item, ViewId, + register_followable_item, Pane, ViewId, Workspace, WorkspaceId, }; actions!(channel_view, [Deploy]); @@ -32,14 +34,47 @@ pub struct ChannelView { } impl ChannelView { + pub fn open( + channel_id: ChannelId, + pane: ViewHandle, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let workspace = workspace.read(cx); + let project = workspace.project().to_owned(); + let channel_store = workspace.app_state().channel_store.clone(); + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + let channel_buffer = + channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); + + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await?; + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx); + }) + }); + + pane.update(&mut cx, |pane, cx| { + pane.items_of_type::() + .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer) + .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx))) + }) + .ok_or_else(|| anyhow!("pane was dropped")) + }) + } + pub fn new( project: ModelHandle, channel_buffer: ModelHandle, - language: Option>, cx: &mut ViewContext, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); - buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); + // buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); @@ -157,6 +192,14 @@ impl Item for ChannelView { }); Label::new(channel_name, style.label.to_owned()).into_any() } + + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { + Some(Self::new( + self.project.clone(), + self.channel_buffer.clone(), + cx, + )) + } } impl FollowableItem for ChannelView { @@ -180,7 +223,7 @@ impl FollowableItem for ChannelView { } fn from_state_proto( - _: ViewHandle, + pane: ViewHandle, workspace: ViewHandle, remote_id: workspace::ViewId, state: &mut Option, @@ -189,48 +232,38 @@ impl FollowableItem for ChannelView { let Some(proto::view::Variant::ChannelView(_)) = state else { return None }; let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() }; - let channel_store = &workspace.read(cx).app_state().channel_store.clone(); - let open_channel_buffer = channel_store.update(cx, |store, cx| { - store.open_channel_buffer(state.channel_id, cx) - }); - let project = workspace.read(cx).project().to_owned(); - let language = workspace.read(cx).app_state().languages.clone(); - let get_markdown = language.language_for_name("Markdown"); + let open = ChannelView::open(state.channel_id, pane, workspace, cx); Some(cx.spawn(|mut cx| async move { - let channel_buffer = open_channel_buffer.await?; - let markdown = get_markdown.await?; - - let this = workspace - .update(&mut cx, move |_, cx| { - cx.add_view(|cx| { - let mut this = Self::new(project, channel_buffer, Some(markdown), cx); - this.remote_id = Some(remote_id); - this - }) + let this = open.await?; + + let task = this + .update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); + + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } }) - .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?; - - if let Some(state) = state.editor { - let task = this.update(&mut cx, |this, cx| { - this.editor.update(cx, |editor, cx| { - editor.apply_update_proto( - &this.project, - proto::update_view::Variant::Editor(proto::update_view::Editor { - selections: state.selections, - pending_selection: state.pending_selection, - scroll_top_anchor: state.scroll_top_anchor, - scroll_x: state.scroll_x, - scroll_y: state.scroll_y, - ..Default::default() - }), - cx, - ) - }) - }); - if let Some(task) = task { - task.await?; - } + .ok_or_else(|| anyhow!("window was closed"))?; + + if let Some(task) = task { + task.await?; } Ok(this) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index a6bd09e43bd2a31877914da1c221f702ff7cc230..5bdc6ad6b7f8b277e04e990a19a87f1893822a1c 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2220,38 +2220,18 @@ impl CollabPanel { } fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { - let workspace = self.workspace; - let open = self.channel_store.update(cx, |channel_store, cx| { - channel_store.open_channel_buffer(action.channel_id, cx) - }); - - cx.spawn(|_, mut cx| async move { - let channel_buffer = open.await?; - - let markdown = workspace - .read_with(&cx, |workspace, _| { - workspace - .app_state() - .languages - .language_for_name("Markdown") - })? - .await?; - - workspace.update(&mut cx, |workspace, cx| { - let channel_view = cx.add_view(|cx| { - ChannelView::new( - workspace.project().to_owned(), - channel_buffer, - Some(markdown), - cx, - ) + if let Some(workspace) = self.workspace.upgrade(cx) { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx); + cx.spawn(|_, mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + pane.add_item(Box::new(channel_view), true, true, None, cx) }); - workspace.add_item(Box::new(channel_view), cx); - })?; - - anyhow::Ok(()) - }) - .detach(); + anyhow::Ok(()) + }) + .detach(); + } } fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { From a327320f7dd317b40ea397d2cbc43fefc40dbc62 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 15:00:54 -0700 Subject: [PATCH 048/142] Show channel notes in current call section of collab panel Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 188 +++++++++++++++++++++------ 1 file changed, 146 insertions(+), 42 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 5bdc6ad6b7f8b277e04e990a19a87f1893822a1c..dece04cb8b2cd5e5c6bd1a7a462c642bb4089ed0 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -18,13 +18,14 @@ use gpui::{ MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, Stack, Svg, }, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, - serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, + serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; @@ -183,6 +184,7 @@ pub struct CollabPanel { #[derive(Serialize, Deserialize)] struct SerializedChannelsPanel { width: Option, + collapsed_channels: Vec, } #[derive(Debug)] @@ -227,6 +229,9 @@ enum ListEntry { channel: Arc, depth: usize, }, + ChannelNotes { + channel_id: ChannelId, + }, ChannelEditor { depth: usize, }, @@ -370,6 +375,12 @@ impl CollabPanel { return channel_row; } } + ListEntry::ChannelNotes { channel_id } => this.render_channel_notes( + *channel_id, + &theme.collab_panel, + is_selected, + cx, + ), ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), this.channel_store.clone(), @@ -509,6 +520,7 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; + panel.collapsed_channels = serialized_panel.collapsed_channels; cx.notify(); }); } @@ -519,12 +531,16 @@ impl CollabPanel { fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; + let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background().spawn( async move { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), - serde_json::to_string(&SerializedChannelsPanel { width })?, + serde_json::to_string(&SerializedChannelsPanel { + width, + collapsed_channels, + })?, ) .await?; anyhow::Ok(()) @@ -548,6 +564,10 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); + if let Some(channel_id) = room.channel_id() { + self.entries.push(ListEntry::ChannelNotes { channel_id }) + } + // Populate the active user. if let Some(user) = user_store.current_user() { self.match_candidates.clear(); @@ -1007,25 +1027,19 @@ impl CollabPanel { ) -> AnyElement { enum JoinProject {} - let font_cache = cx.font_cache(); - let host_avatar_height = theme + let host_avatar_width = theme .contact_avatar .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; let project_name = if worktree_root_names.is_empty() { "untitled".to_string() } else { worktree_root_names.join(", ") }; - MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -1033,39 +1047,20 @@ impl CollabPanel { .style_for(mouse_state); Flex::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = - bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - })) + Svg::new("icons/file_icons/folder.svg") + .with_color(theme.channel_hash.color) .constrained() - .with_width(host_avatar_height), + .with_width(theme.channel_hash.width) + .aligned() + .left(), ) .with_child( Label::new(project_name, row.name.text.clone()) @@ -1240,7 +1235,7 @@ impl CollabPanel { }); if let Some(name) = channel_name { - Cow::Owned(format!("Current Call - #{}", name)) + Cow::Owned(format!("#{}", name)) } else { Cow::Borrowed("Current Call") } @@ -1676,6 +1671,61 @@ impl CollabPanel { .into_any() } + fn render_channel_notes( + &self, + channel_id: ChannelId, + theme: &theme::CollabPanel, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum ChannelNotes {} + let host_avatar_width = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + + MouseEventHandler::new::(channel_id as usize, cx, |state, cx| { + let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + let row = theme.project_row.in_state(is_selected).style_for(state); + + Flex::::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + true, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) + .with_child( + Svg::new("icons/radix/file.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new("notes", theme.channel_name.text.clone()) + .contained() + .with_style(theme.channel_name.container) + .aligned() + .left() + .flex(1., true), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.channel_row.style_for(is_selected, state)) + .with_padding_left(theme.channel_row.default_style().padding.left) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .into_any() + } + fn render_channel_invite( channel: Arc, channel_store: ModelHandle, @@ -2114,6 +2164,7 @@ impl CollabPanel { self.collapsed_channels.insert(ix, channel_id); } }; + self.serialize(cx); self.update_entries(true, cx); cx.notify(); cx.focus_self(); @@ -2392,6 +2443,51 @@ impl CollabPanel { } } +fn render_tree_branch( + branch_style: theme::TreeBranch, + row_style: &TextStyle, + is_last: bool, + size: Vector2F, + font_cache: &FontCache, +) -> gpui::elements::ConstrainedBox { + let line_height = row_style.line_height(font_cache); + let cap_height = row_style.cap_height(font_cache); + let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.; + + Canvas::new(move |scene, bounds, _, _, _| { + scene.paint_layer(None, |scene| { + let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + branch_style.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(branch_style.color), + border: gpui::Border::default(), + corner_radii: (0.).into(), + }); + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + branch_style.width), + ), + background: Some(branch_style.color), + border: gpui::Border::default(), + corner_radii: (0.).into(), + }); + }) + }) + .constrained() + .with_width(size.x()) +} + impl View for CollabPanel { fn ui_name() -> &'static str { "CollabPanel" @@ -2601,6 +2697,14 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id && depth_1 == depth_2; } } + ListEntry::ChannelNotes { channel_id } => { + if let ListEntry::ChannelNotes { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } ListEntry::ChannelInvite(channel_1) => { if let ListEntry::ChannelInvite(channel_2) = other { return channel_1.id == channel_2.id; From 358a20494c177799474baf4c25ab666104c0bb19 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 16:50:13 -0700 Subject: [PATCH 049/142] Make channel notes read-only when disconnected Co-authored-by: Mikayla --- crates/channel/src/channel_buffer.rs | 58 +++++---- crates/channel/src/channel_store.rs | 115 ++++++++++++------ crates/collab/src/rpc.rs | 5 +- .../collab/src/tests/channel_buffer_tests.rs | 76 ++++++++++++ crates/collab/src/tests/channel_tests.rs | 7 +- crates/collab_ui/src/channel_view.rs | 36 +++--- 6 files changed, 217 insertions(+), 80 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index c19899501a43a360d1a4b2140c60d09f4a2fc608..29f4d3493c6d0fe9e2fc041695f40fe48225c76f 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,4 +1,4 @@ -use crate::{Channel, ChannelId, ChannelStore}; +use crate::Channel; use anyhow::Result; use client::Client; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; @@ -13,39 +13,43 @@ pub(crate) fn init(client: &Arc) { } pub struct ChannelBuffer { - channel_id: ChannelId, + pub(crate) channel: Arc, + connected: bool, collaborators: Vec, buffer: ModelHandle, - channel_store: ModelHandle, client: Arc, - _subscription: client::Subscription, + subscription: Option, } pub enum Event { CollaboratorsChanged, + Disconnected, } impl Entity for ChannelBuffer { type Event = Event; fn release(&mut self, _: &mut AppContext) { - self.client - .send(proto::LeaveChannelBuffer { - channel_id: self.channel_id, - }) - .log_err(); + if self.connected { + self.client + .send(proto::LeaveChannelBuffer { + channel_id: self.channel.id, + }) + .log_err(); + } } } impl ChannelBuffer { pub(crate) async fn new( - channel_store: ModelHandle, - channel_id: ChannelId, + channel: Arc, client: Arc, mut cx: AsyncAppContext, ) -> Result> { let response = client - .request(proto::JoinChannelBuffer { channel_id }) + .request(proto::JoinChannelBuffer { + channel_id: channel.id, + }) .await?; let base_text = response.base_text; @@ -62,7 +66,7 @@ impl ChannelBuffer { }); buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; - let subscription = client.subscribe_to_entity(channel_id)?; + let subscription = client.subscribe_to_entity(channel.id)?; anyhow::Ok(cx.add_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); @@ -70,10 +74,10 @@ impl ChannelBuffer { Self { buffer, client, - channel_id, - channel_store, + connected: true, collaborators, - _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), + channel, + subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), } })) } @@ -155,7 +159,7 @@ impl ChannelBuffer { let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { - channel_id: self.channel_id, + channel_id: self.channel.id, operations: vec![operation], }) .log_err(); @@ -170,11 +174,21 @@ impl ChannelBuffer { &self.collaborators } - pub fn channel(&self, cx: &AppContext) -> Option> { - self.channel_store - .read(cx) - .channel_for_id(self.channel_id) - .cloned() + pub fn channel(&self) -> Arc { + self.channel.clone() + } + + pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { + if self.connected { + self.connected = false; + self.subscription.take(); + cx.emit(Event::Disconnected); + cx.notify() + } + } + + pub fn is_connected(&self) -> bool { + self.connected } pub fn replica_id(&self, cx: &AppContext) -> u16 { diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 1d83bd1d7f2771d87fec37091e5ade1698d87c86..861f731331ca6337ed7d798162ceb3321ad170fe 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -2,7 +2,7 @@ use crate::channel_buffer::ChannelBuffer; use anyhow::{anyhow, Result}; use client::{Client, Status, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; -use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; +use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; @@ -71,16 +71,14 @@ impl ChannelStore { let mut connection_status = client.status(); let watch_connection_status = cx.spawn_weak(|this, mut cx| async move { while let Some(status) = connection_status.next().await { - if matches!(status, Status::ConnectionLost | Status::SignedOut) { + if !status.is_connected() { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.channels_by_id.clear(); - this.channel_invitations.clear(); - this.channel_participants.clear(); - this.channels_with_admin_privileges.clear(); - this.channel_paths.clear(); - this.outgoing_invites.clear(); - cx.notify(); + if matches!(status, Status::ConnectionLost | Status::SignedOut) { + this.handle_disconnect(cx); + } else { + this.disconnect_buffers(cx); + } }); } else { break; @@ -176,9 +174,17 @@ impl ChannelStore { OpenedChannelBuffer::Loading(task) => break task.clone(), }, hash_map::Entry::Vacant(e) => { + let client = self.client.clone(); let task = cx - .spawn(|this, cx| { - ChannelBuffer::new(this, channel_id, self.client.clone(), cx) + .spawn(|this, cx| async move { + let channel = this.read_with(&cx, |this, _| { + this.channel_for_id(channel_id).cloned().ok_or_else(|| { + Arc::new(anyhow!("no channel for id: {}", channel_id)) + }) + })?; + + ChannelBuffer::new(channel, client, cx) + .await .map_err(Arc::new) }) .shared(); @@ -187,8 +193,8 @@ impl ChannelStore { let task = task.clone(); |this, mut cx| async move { let result = task.await; - this.update(&mut cx, |this, cx| { - if let Ok(buffer) = result { + this.update(&mut cx, |this, cx| match result { + Ok(buffer) => { cx.observe_release(&buffer, move |this, _, _| { this.opened_buffers.remove(&channel_id); }) @@ -197,7 +203,9 @@ impl ChannelStore { channel_id, OpenedChannelBuffer::Open(buffer.downgrade()), ); - } else { + } + Err(error) => { + log::error!("failed to open channel buffer {error:?}"); this.opened_buffers.remove(&channel_id); } }); @@ -474,6 +482,27 @@ impl ChannelStore { Ok(()) } + fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) { + self.disconnect_buffers(cx); + self.channels_by_id.clear(); + self.channel_invitations.clear(); + self.channel_participants.clear(); + self.channels_with_admin_privileges.clear(); + self.channel_paths.clear(); + self.outgoing_invites.clear(); + cx.notify(); + } + + fn disconnect_buffers(&mut self, cx: &mut ModelContext) { + for (_, buffer) in self.opened_buffers.drain() { + if let OpenedChannelBuffer::Open(buffer) = buffer { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); + } + } + } + } + pub(crate) fn update_channels( &mut self, payload: proto::UpdateChannels, @@ -508,38 +537,44 @@ impl ChannelStore { .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); self.channels_with_admin_privileges .retain(|channel_id| !payload.remove_channels.contains(channel_id)); - } - for channel in payload.channels { - if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) { - // FIXME: We may be missing a path for this existing channel in certain cases - let existing_channel = Arc::make_mut(existing_channel); - existing_channel.name = channel.name; - continue; + for channel_id in &payload.remove_channels { + let channel_id = *channel_id; + if let Some(OpenedChannelBuffer::Open(buffer)) = + self.opened_buffers.remove(&channel_id) + { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, ChannelBuffer::disconnect); + } + } } + } - self.channels_by_id.insert( - channel.id, - Arc::new(Channel { - id: channel.id, - name: channel.name, - }), - ); - - if let Some(parent_id) = channel.parent_id { - let mut ix = 0; - while ix < self.channel_paths.len() { - let path = &self.channel_paths[ix]; - if path.ends_with(&[parent_id]) { - let mut new_path = path.clone(); - new_path.push(channel.id); - self.channel_paths.insert(ix + 1, new_path); + for channel_proto in payload.channels { + if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { + Arc::make_mut(existing_channel).name = channel_proto.name; + } else { + let channel = Arc::new(Channel { + id: channel_proto.id, + name: channel_proto.name, + }); + self.channels_by_id.insert(channel.id, channel.clone()); + + if let Some(parent_id) = channel_proto.parent_id { + let mut ix = 0; + while ix < self.channel_paths.len() { + let path = &self.channel_paths[ix]; + if path.ends_with(&[parent_id]) { + let mut new_path = path.clone(); + new_path.push(channel.id); + self.channel_paths.insert(ix + 1, new_path); + ix += 1; + } ix += 1; } - ix += 1; + } else { + self.channel_paths.push(vec![channel.id]); } - } else { - self.channel_paths.push(vec![channel.id]); } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 2bd39c861dde899f302531db2f54058dd0b048bc..18587c2ba8f590f3646a3a7de6d4121ffe35586d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -854,10 +854,13 @@ async fn connection_lost( .await .trace_err(); + leave_channel_buffers_for_session(&session) + .await + .trace_err(); + futures::select_biased! { _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { leave_room_for_session(&session).await.trace_err(); - leave_channel_buffers_for_session(&session).await.trace_err(); if !session .connection_pool() diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 0ecd4588c5f640b22490de7b0d101a04cc66aac4..8ac4dbbd3f1c606b52fb445a1c08ca4f1e8c6883 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,5 +1,6 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; +use channel::Channel; use client::UserId; use collab_ui::channel_view::ChannelView; use collections::HashMap; @@ -334,6 +335,81 @@ async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mu }); } +#[gpui::test] +async fn test_channel_buffer_disconnect( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + channel_buffer_a.update(cx_a, |buffer, _| { + assert_eq!( + buffer.channel().as_ref(), + &Channel { + id: channel_id, + name: "zed".to_string() + } + ); + assert!(!buffer.is_connected()); + }); + + deterministic.run_until_parked(); + + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + deterministic.run_until_parked(); + + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_id) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Channel buffer observed the deletion + channel_buffer_b.update(cx_b, |buffer, _| { + assert_eq!( + buffer.channel().as_ref(), + &Channel { + id: channel_id, + name: "zed".to_string() + } + ); + assert!(!buffer.is_connected()); + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 41d228677227068ded7f50df3902928d7033caef..b54b4d349ba54e5c23e048cd81b292a10566445d 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -799,7 +799,7 @@ async fn test_lost_channel_creation( deterministic.run_until_parked(); - // Sanity check + // Sanity check, B has the invitation assert_channel_invitations( client_b.channel_store(), cx_b, @@ -811,6 +811,7 @@ async fn test_lost_channel_creation( }], ); + // A creates a subchannel while the invite is still pending. let subchannel_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { @@ -841,7 +842,7 @@ async fn test_lost_channel_creation( ], ); - // Accept the invite + // Client B accepts the invite client_b .channel_store() .update(cx_b, |channel_store, _| { @@ -852,7 +853,7 @@ async fn test_lost_channel_creation( deterministic.run_until_parked(); - // B should now see the channel + // Client B should now see the channel assert_channels( client_b.channel_store(), cx_b, diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 0e2d3636aa281c23ec800788140c37351faba563..9c125117e10e7fc6be2b9fc49414dd0578c95a22 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -114,10 +114,18 @@ impl ChannelView { fn handle_channel_buffer_event( &mut self, _: ModelHandle, - _: &channel_buffer::Event, + event: &channel_buffer::Event, cx: &mut ViewContext, ) { - self.refresh_replica_id_map(cx); + match event { + channel_buffer::Event::CollaboratorsChanged => { + self.refresh_replica_id_map(cx); + } + channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + } } /// Build a mapping of channel buffer replica ids to the corresponding @@ -183,14 +191,13 @@ impl Item for ChannelView { style: &theme::Tab, cx: &gpui::AppContext, ) -> AnyElement { - let channel_name = self - .channel_buffer - .read(cx) - .channel(cx) - .map_or("[Deleted channel]".to_string(), |channel| { - format!("#{}", channel.name) - }); - Label::new(channel_name, style.label.to_owned()).into_any() + let channel_name = &self.channel_buffer.read(cx).channel().name; + let label = if self.channel_buffer.read(cx).is_connected() { + format!("#{}", channel_name) + } else { + format!("#{} (disconnected)", channel_name) + }; + Label::new(label, style.label.to_owned()).into_any() } fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { @@ -208,8 +215,9 @@ impl FollowableItem for ChannelView { } fn to_state_proto(&self, cx: &AppContext) -> Option { - self.channel_buffer.read(cx).channel(cx).map(|channel| { - proto::view::Variant::ChannelView(proto::view::ChannelView { + let channel = self.channel_buffer.read(cx).channel(); + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { channel_id: channel.id, editor: if let Some(proto::view::Variant::Editor(proto)) = self.editor.read(cx).to_state_proto(cx) @@ -218,8 +226,8 @@ impl FollowableItem for ChannelView { } else { None }, - }) - }) + }, + )) } fn from_state_proto( From c7c220309dbfdf8e90625dd0a47401c6da5019a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 16:55:39 -0700 Subject: [PATCH 050/142] Avoid creating redundant snapshots of channel notes buffers Co-authored-by: Mikayla --- crates/collab/src/db/queries/buffers.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index a38693bace24364e00ab54bed79c68a95a30f9f0..354accc01a237057a6ea3bbeb9b4c1986b4ea391 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -326,9 +326,11 @@ impl Database { .ok_or_else(|| anyhow!("no such buffer"))?; let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; + if operations.is_empty() { + return Ok(()); + } let mut text_buffer = text::Buffer::new(0, 0, base_text); - text_buffer .apply_ops(operations.into_iter().filter_map(operation_from_wire)) .unwrap(); From 7b6c0c539c9f8a8e30f98413269f8d17a1b7c224 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 17:17:20 -0700 Subject: [PATCH 051/142] Show non-admin context menu items for all channel members Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 79 +++++++++++++++------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index dece04cb8b2cd5e5c6bd1a7a462c642bb4089ed0..411a3a2598c052dfb92f4df438effa1c1e57270a 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1923,47 +1923,52 @@ impl CollabPanel { channel_id: u64, cx: &mut ViewContext, ) { - if self.channel_store.read(cx).is_user_admin(channel_id) { - self.context_menu_on_selected = position.is_none(); + self.context_menu_on_selected = position.is_none(); - self.context_menu.update(cx, |context_menu, cx| { - context_menu.set_position_mode(if self.context_menu_on_selected { - OverlayPositionMode::Local - } else { - OverlayPositionMode::Window - }); + self.context_menu.update(cx, |context_menu, cx| { + context_menu.set_position_mode(if self.context_menu_on_selected { + OverlayPositionMode::Local + } else { + OverlayPositionMode::Window + }); - let expand_action_name = if self.is_channel_collapsed(channel_id) { - "Expand Subchannels" - } else { - "Collapse Subchannels" - }; + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; - context_menu.show( - position.unwrap_or_default(), - if self.context_menu_on_selected { - gpui::elements::AnchorCorner::TopRight - } else { - gpui::elements::AnchorCorner::BottomLeft - }, - vec![ - ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), - ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), - ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Rename", RenameChannel { channel_id }), - ContextMenuItem::action("Manage", ManageMembers { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Delete", RemoveChannel { channel_id }), - ], - cx, - ); - }); + let mut items = vec![ + ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), + ]; + + if self.channel_store.read(cx).is_user_admin(channel_id) { + items.extend([ + ContextMenuItem::Separator, + ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), + ContextMenuItem::action("Rename", RenameChannel { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Invite Members", InviteMembers { channel_id }), + ContextMenuItem::action("Manage Members", ManageMembers { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Delete", RemoveChannel { channel_id }), + ]); + } - cx.notify(); - } + context_menu.show( + position.unwrap_or_default(), + if self.context_menu_on_selected { + gpui::elements::AnchorCorner::TopRight + } else { + gpui::elements::AnchorCorner::BottomLeft + }, + items, + cx, + ); + }); + + cx.notify(); } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { From a95dcfa8bc74dbfec05174af5f9b1498737862b5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 17:18:18 -0700 Subject: [PATCH 052/142] Make channel notes view searchable and navigable via pane history Co-authored-by: Mikayla --- crates/collab_ui/src/channel_view.rs | 37 +++++++++++++++++++++++++++- crates/editor/src/items.rs | 2 +- crates/vim/src/visual.rs | 12 ++++----- crates/workspace/src/item.rs | 8 +++--- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 9c125117e10e7fc6be2b9fc49414dd0578c95a22..bb1e840ffca40087519d324db5a2ae9a62a38222 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -10,13 +10,17 @@ use editor::Editor; use gpui::{ actions, elements::{ChildView, Label}, + geometry::vector::Vector2F, AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, }; use project::Project; +use std::any::Any; use workspace::{ item::{FollowableItem, Item, ItemHandle}, - register_followable_item, Pane, ViewId, Workspace, WorkspaceId, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId, }; actions!(channel_view, [Deploy]); @@ -207,6 +211,37 @@ impl Item for ChannelView { cx, )) } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + true + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } + + fn as_searchable(&self, _: &ViewHandle) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.editor.read(cx).pixel_position_of_cursor(cx) + } } impl FollowableItem for ChannelView { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 657aae5ff9df1c4707ba42d3d188e3f9fa391a5e..477eab41ac9cc7a0c7b38e0ec07f3eb41f46963e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -754,7 +754,7 @@ impl Item for Editor { Some(Box::new(handle.clone())) } - fn pixel_position_of_cursor(&self) -> Option { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { self.pixel_position_of_newest_cursor } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1a11721a4e3c7cd9fb2456c00ac151387b55f57b..5e60ef59fc1b6fe41f91f0decffc64b698220c6f 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -391,7 +391,7 @@ mod test { the lazy dog" }) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); // entering visual mode should select the character // under cursor @@ -400,7 +400,7 @@ mod test { fox jumps over the lazy dog"}) .await; - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); // forwards motions should extend the selection cx.simulate_shared_keystrokes(["w", "j"]).await; @@ -430,7 +430,7 @@ mod test { b "}) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); cx.simulate_shared_keystrokes(["v"]).await; cx.assert_shared_state(indoc! {" a @@ -438,7 +438,7 @@ mod test { ˇ»b "}) .await; - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); // toggles off again cx.simulate_shared_keystrokes(["v"]).await; @@ -510,7 +510,7 @@ mod test { b ˇ"}) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); cx.simulate_shared_keystrokes(["shift-v"]).await; cx.assert_shared_state(indoc! {" a @@ -518,7 +518,7 @@ mod test { ˇ"}) .await; assert_eq!(cx.mode(), cx.neovim_mode().await); - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); cx.simulate_shared_keystrokes(["x"]).await; cx.assert_shared_state(indoc! {" a diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 4b5b7a793118981529c2b7bfb3d0bbaa94121660..c218a85234918d424526373b5fc5494316533283 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -158,9 +158,7 @@ pub trait Item: View { fn should_update_tab_on_event(_: &Self::Event) -> bool { false } - fn is_edit_event(_: &Self::Event) -> bool { - false - } + fn act_as_type<'a>( &'a self, type_id: TypeId, @@ -205,7 +203,7 @@ pub trait Item: View { fn show_toolbar(&self) -> bool { true } - fn pixel_position_of_cursor(&self) -> Option { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { None } } @@ -623,7 +621,7 @@ impl ItemHandle for ViewHandle { } fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { - self.read(cx).pixel_position_of_cursor() + self.read(cx).pixel_position_of_cursor(cx) } } From 9fe580acb675e573588e9451617310e26be8e9ea Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 25 Aug 2023 01:50:54 -0400 Subject: [PATCH 053/142] WIP --- crates/project/src/terminals.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 539d120e971ecb562eaaa28fdccb6d084c49bba6..2fb66a6c4c95b06acf4a305ec6652d2f2ee3a653 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -77,7 +77,7 @@ impl Project { let program = match shell { terminal::Shell::System => "Figure this out", terminal::Shell::Program(program) => program, - terminal::Shell::WithArguments { program, args } => program, + terminal::Shell::WithArguments { program, args: _ } => program, }; // This is so hacky - find a better way to do this From 8288e5591d893ec54e7621aea36062452f3fbd11 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 25 Aug 2023 02:21:07 -0400 Subject: [PATCH 054/142] Automatically enable project search filters when using `Search Inside` --- crates/search/src/project_search.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 52247b9bc0d1cc6b55fb876ecc0549a29eb4d1a6..6bd48138ac79462008cee91cf3c0403c77322120 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -893,6 +893,7 @@ impl ProjectSearchView { search .included_files_editor .update(cx, |editor, cx| editor.set_text(filter_str, cx)); + search.filters_enabled = true; search.focus_query_editor(cx) }); } From ee97bc54cfc184d90dfbaff2bac585b8e60df202 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 25 Aug 2023 10:38:01 +0200 Subject: [PATCH 055/142] cleaned up warnings --- crates/search/src/project_search.rs | 2 +- crates/semantic_index/src/semantic_index.rs | 40 +++++++++---------- .../src/semantic_index_tests.rs | 2 +- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f665c4ddcdf585c4373406bbef12a01d88a2ceca..a29fad0f548fe0f7fdc0e8526b9aa5615873ea57 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -760,7 +760,7 @@ impl ProjectSearchView { } fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { - let mut project; + let project; let excerpts; let mut query_text = String::new(); let mut options = SearchOptions::NONE; diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 474025c25e3d435d36518c6125396e78ac6aba55..70495b59d30cc9bb47eb9aa66e6cf22e73d720da 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -12,11 +12,9 @@ use db::VectorDatabase; use embedding::{EmbeddingProvider, OpenAIEmbeddings}; use futures::{channel::oneshot, Future}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; -use isahc::http::header::OccupiedEntry; use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; -use postage::stream::Stream; use postage::watch; use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, WorktreeId}; use smol::channel; @@ -58,7 +56,7 @@ pub fn init( } cx.subscribe_global::({ - move |event, mut cx| { + move |event, cx| { let Some(semantic_index) = SemanticIndex::global(cx) else { return; }; let workspace = &event.0; if let Some(workspace) = workspace.upgrade(cx) { @@ -111,7 +109,7 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, - subscription: gpui::Subscription, + _subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, job_queue_tx: channel::Sender, @@ -141,9 +139,6 @@ impl ProjectState { outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, ) -> Self { - let (job_count_tx, job_count_rx) = watch::channel_with(0); - let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - let (job_queue_tx, job_queue_rx) = channel::unbounded(); let _queue_update_task = cx.background().spawn({ let mut worktree_queue = HashMap::new(); @@ -158,7 +153,7 @@ impl ProjectState { worktree_db_ids, outstanding_job_count_rx, _outstanding_job_count_tx, - subscription, + _subscription: subscription, _queue_update_task, job_queue_tx, } @@ -175,18 +170,18 @@ impl ProjectState { for (_, op) in queue { match op { IndexOperation::IndexFile { - absolute_path, + absolute_path: _, payload, tx, } => { - tx.try_send(payload); + let _ = tx.try_send(payload); } IndexOperation::DeleteFile { - absolute_path, + absolute_path: _, payload, tx, } => { - tx.try_send(payload); + let _ = tx.try_send(payload); } _ => {} } @@ -715,7 +710,7 @@ impl SemanticIndex { .ok_or(anyhow!("Worktree not available"))? .read(cx) .snapshot(); - cx.spawn(|this, mut cx| async move { + cx.spawn(|_, _| async move { let worktree = worktree.clone(); for (path, entry_id, path_change) in changes.iter() { let relative_path = path.to_path_buf(); @@ -758,7 +753,7 @@ impl SemanticIndex { }, tx: parsing_files_tx.clone(), }; - job_queue_tx.try_send(new_operation); + let _ = job_queue_tx.try_send(new_operation); } } PathChange::Removed => { @@ -770,7 +765,7 @@ impl SemanticIndex { }, tx: db_update_tx.clone(), }; - job_queue_tx.try_send(new_operation); + let _ = job_queue_tx.try_send(new_operation); } _ => {} } @@ -808,7 +803,8 @@ impl SemanticIndex { let _subscription = cx.subscribe(&project, |this, project, event, cx| { if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { - this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id); + let _ = + this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id); }; }); @@ -901,7 +897,7 @@ impl SemanticIndex { } } // Clean up entries from database that are no longer in the worktree. - for (path, mtime) in file_mtimes { + for (path, _) in file_mtimes { worktree_files.push(IndexOperation::DeleteFile { absolute_path: worktree.absolutize(path.as_path()), payload: DbOperation::Delete { @@ -927,7 +923,7 @@ impl SemanticIndex { ); for op in worktree_files { - project_state.job_queue_tx.try_send(op); + let _ = project_state.job_queue_tx.try_send(op); } this.projects.insert(project.downgrade(), project_state); @@ -948,17 +944,17 @@ impl SemanticIndex { state.unwrap() }; - let parsing_files_tx = self.parsing_files_tx.clone(); - let db_update_tx = self.db_update_tx.clone(); + // let parsing_files_tx = self.parsing_files_tx.clone(); + // let db_update_tx = self.db_update_tx.clone(); let job_count_rx = state.outstanding_job_count_rx.clone(); let count = state.get_outstanding_count(); cx.spawn(|this, mut cx| async move { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, _| { let Some(state) = this.projects.get_mut(&project.downgrade()) else { return; }; - state.job_queue_tx.try_send(IndexOperation::FlushQueue); + let _ = state.job_queue_tx.try_send(IndexOperation::FlushQueue); }); Ok((count, job_count_rx)) diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 0ac5953f0bb2a5771cc9c962ee095208b7895c10..32d8bb0fb879fe9e1dcf69713d73dbcdc722ffcb 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -87,7 +87,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) { let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; - store + let _ = store .update(cx, |store, cx| { store.initialize_project(project.clone(), cx) }) From d34491e822e6344d8dc186a7a937aac7be4957ca Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 16 Aug 2023 14:21:26 +0300 Subject: [PATCH 056/142] Draft inlay hint part hover detection --- crates/editor/src/display_map.rs | 19 +++- crates/editor/src/element.rs | 122 +++++++++++++++++++++----- crates/editor/src/inlay_hint_cache.rs | 11 +++ 3 files changed, 125 insertions(+), 27 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aee41e6c53f0c1e35e68e3716db55f609a44fb08..9159253142e56c55ed1243e41a12ea79e7ca26ff 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -27,7 +27,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::inlay_map::Inlay; +pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { @@ -387,12 +387,25 @@ impl DisplaySnapshot { } fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { + self.inlay_snapshot + .to_buffer_point(self.display_point_to_inlay_point(point, bias)) + } + + pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset { + self.inlay_snapshot + .to_offset(self.display_point_to_inlay_point(point, bias)) + } + + pub fn inlay_point_to_inlay_offset(&self, point: InlayPoint) -> InlayOffset { + self.inlay_snapshot.to_offset(point) + } + + fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point); let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0; - let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); - self.inlay_snapshot.to_buffer_point(inlay_point) + fold_point.to_inlay_point(&self.fold_snapshot) } pub fn max_point(&self) -> DisplayPoint { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9f74eed790fa6b025bdd12125858ad02ec44d8ac..e2aaa3a3bbfc60a6cc954dd93a261e878fdb2557 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayPoint, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -42,7 +42,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - ProjectPath, + InlayHintLabelPart, ProjectPath, }; use smallvec::SmallVec; use std::{ @@ -455,11 +455,67 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let point = position_to_display_point(position, text_bounds, position_map); - - update_go_to_definition_link(editor, point, cmd, shift, cx); - hover_at(editor, point, cx); + if text_bounds.contains_point(position) { + let (nearest_valid_position, unclipped_position) = + position_map.point_for_position(text_bounds, position); + if nearest_valid_position == unclipped_position { + update_go_to_definition_link(editor, Some(nearest_valid_position), cmd, shift, cx); + hover_at(editor, Some(nearest_valid_position), cx); + return true; + } else { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_position = position_map + .snapshot + .clip_point(unclipped_position, Bias::Left) + .to_point(&position_map.snapshot.display_snapshot); + let previous_valid_anchor = snapshot.anchor_at(previous_valid_position, Bias::Left); + let next_valid_position = position_map + .snapshot + .clip_point(unclipped_position, Bias::Right) + .to_point(&position_map.snapshot.display_snapshot); + let next_valid_anchor = snapshot.anchor_at(next_valid_position, Bias::Right); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) + { + if let Some(cached_hint) = editor + .inlay_hint_cache() + .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.label { + project::InlayHintLabel::String(regular_label) => { + // TODO kb remove + check for tooltip for hover and resolve, if needed + eprintln!("regular string: {regular_label}"); + } + project::InlayHintLabel::LabelParts(label_parts) => { + // TODO kb how to properly convert it? + let unclipped_inlay_position = InlayPoint::new( + unclipped_position.row(), + unclipped_position.column(), + ); + if let Some(hovered_hint_part) = find_hovered_hint_part( + &position_map.snapshot, + &label_parts, + previous_valid_position, + next_valid_position, + unclipped_inlay_position, + ) { + // TODO kb remove + check for tooltip and location and resolve, if needed + eprintln!("hint_part: {hovered_hint_part:?}"); + } + } + }; + } + } + } + }; + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); true } @@ -909,7 +965,7 @@ impl EditorElement { &text, cursor_row_layout.font_size(), &[( - text.len(), + text.chars().count(), RunStyle { font_id, color: style.background, @@ -1804,6 +1860,40 @@ impl EditorElement { } } +fn find_hovered_hint_part<'a>( + snapshot: &EditorSnapshot, + label_parts: &'a [InlayHintLabelPart], + hint_start: Point, + hint_end: Point, + hovered_position: InlayPoint, +) -> Option<&'a InlayHintLabelPart> { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(hint_start.to_display_point(&snapshot), Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(hint_end.to_display_point(&snapshot), Bias::Right); + dbg!(( + "~~~~~~~~~", + hint_start, + hint_start_offset, + hint_end, + hint_end_offset, + hovered_position + )); + let hovered_offset = snapshot.inlay_point_to_inlay_offset(hovered_position); + if hovered_offset >= hint_start_offset && hovered_offset <= hint_end_offset { + let mut hovered_character = (hovered_offset - hint_start_offset).0; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character >= part_len { + hovered_character -= part_len; + } else { + return Some(part); + } + } + } + None +} + struct HighlightedChunk<'a> { chunk: &'a str, style: Option, @@ -2663,6 +2753,7 @@ impl PositionMap { let mut target_point = DisplayPoint::new(row, column); let point = self.snapshot.clip_point(target_point, Bias::Left); + // TODO kb looks wrong, need to construct inlay point instead? operate offsets? *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; (point, target_point) @@ -2919,23 +3010,6 @@ impl HighlightedRange { } } -fn position_to_display_point( - position: Vector2F, - text_bounds: RectF, - position_map: &PositionMap, -) -> Option { - if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } - } else { - None - } -} - fn range_to_bounds( range: &Range, content_origin: Vector2F, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 70cccf21da391d9308ece455d0de09768f68f280..5b9bdd08ec25d1d2c872fa74bce9d6ab865d2781 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -386,6 +386,17 @@ impl InlayHintCache { self.hints.clear(); } + pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { + self.hints + .get(&excerpt_id)? + .read() + .hints + .iter() + .find(|&(id, _)| id == &hint_id) + .map(|(_, hint)| hint) + .cloned() + } + pub fn hints(&self) -> Vec { let mut hints = Vec::new(); for excerpt_hints in self.hints.values() { From d1cb0b3c27fcbe1b02df3e4eae5426fb15802622 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 11:56:38 +0300 Subject: [PATCH 057/142] Properly detect hovered inlay hint label part --- crates/editor/src/element.rs | 221 +++++++++++++++++++---------------- 1 file changed, 119 insertions(+), 102 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e2aaa3a3bbfc60a6cc954dd93a261e878fdb2557..57384195519f801c78b841608f772bbde1994782 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayPoint, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -287,13 +287,13 @@ impl EditorElement { return false; } - let (position, target_position) = position_map.point_for_position(text_bounds, position); - + let point_for_position = position_map.point_for_position(text_bounds, position); + let position = point_for_position.previous_valid; if shift && alt { editor.select( SelectPhase::BeginColumnar { position, - goal_column: target_position.column(), + goal_column: point_for_position.exact_unclipped.column(), }, cx, ); @@ -329,9 +329,13 @@ impl EditorElement { if !text_bounds.contains_point(position) { return false; } - - let (point, _) = position_map.point_for_position(text_bounds, position); - mouse_context_menu::deploy_context_menu(editor, position, point, cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + mouse_context_menu::deploy_context_menu( + editor, + position, + point_for_position.previous_valid, + cx, + ); true } @@ -353,9 +357,10 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - - if point == target_point { + if let Some(point) = position_map + .point_for_position(text_bounds, position) + .as_valid() + { if shift { go_to_fetched_type_definition(editor, point, alt, cx); } else { @@ -383,12 +388,9 @@ impl EditorElement { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu let point = if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } + position_map + .point_for_position(text_bounds, position) + .as_valid() } else { None }; @@ -422,13 +424,12 @@ impl EditorElement { )) } - let (position, target_position) = - position_map.point_for_position(text_bounds, position); + let point_for_position = position_map.point_for_position(text_bounds, position); editor.select( SelectPhase::Update { - position, - goal_column: target_position.column(), + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), scroll_position: (position_map.snapshot.scroll_position() + scroll_delta) .clamp(Vector2F::zero(), position_map.scroll_max), }, @@ -456,59 +457,74 @@ impl EditorElement { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu if text_bounds.contains_point(position) { - let (nearest_valid_position, unclipped_position) = - position_map.point_for_position(text_bounds, position); - if nearest_valid_position == unclipped_position { - update_go_to_definition_link(editor, Some(nearest_valid_position), cmd, shift, cx); - hover_at(editor, Some(nearest_valid_position), cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + if let Some(point) = point_for_position.as_valid() { + update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + hover_at(editor, Some(point), cx); return true; } else { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let previous_valid_position = position_map + let hint_start_offset = position_map .snapshot - .clip_point(unclipped_position, Bias::Left) - .to_point(&position_map.snapshot.display_snapshot); - let previous_valid_anchor = snapshot.anchor_at(previous_valid_position, Bias::Left); - let next_valid_position = position_map + .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = position_map .snapshot - .clip_point(unclipped_position, Bias::Right) - .to_point(&position_map.snapshot.display_snapshot); - let next_valid_anchor = snapshot.anchor_at(next_valid_position, Bias::Right); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - if let Some(cached_hint) = editor - .inlay_hint_cache() - .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some(position_map.snapshot.display_point_to_inlay_offset( + point_for_position.exact_unclipped, + Bias::Left, + )) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + if let Some(hovered_offset) = hovered_offset { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_anchor = snapshot.anchor_at( + point_for_position + .previous_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Left, + ); + let next_valid_anchor = snapshot.anchor_at( + point_for_position + .next_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt() + }) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) { - match &cached_hint.label { - project::InlayHintLabel::String(regular_label) => { - // TODO kb remove + check for tooltip for hover and resolve, if needed - eprintln!("regular string: {regular_label}"); - } - project::InlayHintLabel::LabelParts(label_parts) => { - // TODO kb how to properly convert it? - let unclipped_inlay_position = InlayPoint::new( - unclipped_position.row(), - unclipped_position.column(), - ); - if let Some(hovered_hint_part) = find_hovered_hint_part( - &position_map.snapshot, - &label_parts, - previous_valid_position, - next_valid_position, - unclipped_inlay_position, - ) { - // TODO kb remove + check for tooltip and location and resolve, if needed - eprintln!("hint_part: {hovered_hint_part:?}"); + if let Some(cached_hint) = editor + .inlay_hint_cache() + .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.label { + project::InlayHintLabel::String(regular_label) => { + // TODO kb remove + check for tooltip for hover and resolve, if needed + eprintln!("regular string: {regular_label}"); } - } - }; + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some(hovered_hint_part) = find_hovered_hint_part( + &label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) { + // TODO kb remove + check for tooltip and location and resolve, if needed + eprintln!("hint_part: {hovered_hint_part:?}"); + } + } + }; + } } } } @@ -1861,27 +1877,12 @@ impl EditorElement { } fn find_hovered_hint_part<'a>( - snapshot: &EditorSnapshot, label_parts: &'a [InlayHintLabelPart], - hint_start: Point, - hint_end: Point, - hovered_position: InlayPoint, + hint_range: Range, + hovered_offset: InlayOffset, ) -> Option<&'a InlayHintLabelPart> { - let hint_start_offset = - snapshot.display_point_to_inlay_offset(hint_start.to_display_point(&snapshot), Bias::Left); - let hint_end_offset = - snapshot.display_point_to_inlay_offset(hint_end.to_display_point(&snapshot), Bias::Right); - dbg!(( - "~~~~~~~~~", - hint_start, - hint_start_offset, - hint_end, - hint_end_offset, - hovered_position - )); - let hovered_offset = snapshot.inlay_point_to_inlay_offset(hovered_position); - if hovered_offset >= hint_start_offset && hovered_offset <= hint_end_offset { - let mut hovered_character = (hovered_offset - hint_start_offset).0; + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { @@ -2722,22 +2723,32 @@ struct PositionMap { snapshot: EditorSnapshot, } +#[derive(Debug)] +struct PointForPosition { + previous_valid: DisplayPoint, + next_valid: DisplayPoint, + exact_unclipped: DisplayPoint, + column_overshoot_after_line_end: u32, +} + +impl PointForPosition { + fn as_valid(&self) -> Option { + if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { + Some(self.previous_valid) + } else { + None + } + } +} + impl PositionMap { - /// Returns two display points: - /// 1. The nearest *valid* position in the editor - /// 2. An unclipped, potentially *invalid* position that maps directly to - /// the given pixel position. - fn point_for_position( - &self, - text_bounds: RectF, - position: Vector2F, - ) -> (DisplayPoint, DisplayPoint) { + fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition { let scroll_position = self.snapshot.scroll_position(); let position = position - text_bounds.origin(); let y = position.y().max(0.0).min(self.size.y()); let x = position.x() + (scroll_position.x() * self.em_width); let row = (y / self.line_height + scroll_position.y()) as u32; - let (column, x_overshoot) = if let Some(line) = self + let (column, x_overshoot_after_line_end) = if let Some(line) = self .line_layouts .get(row as usize - scroll_position.y() as usize) .map(|line_with_spaces| &line_with_spaces.line) @@ -2751,12 +2762,18 @@ impl PositionMap { (0, x) }; - let mut target_point = DisplayPoint::new(row, column); - let point = self.snapshot.clip_point(target_point, Bias::Left); - // TODO kb looks wrong, need to construct inlay point instead? operate offsets? - *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; - - (point, target_point) + let mut exact_unclipped = DisplayPoint::new(row, column); + let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); + let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); + + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32; + *exact_unclipped.column_mut() += column_overshoot_after_line_end; + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end, + } } } From e4b78e322edfea72039d3b2afce0646c4946d9fd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 12:37:05 +0300 Subject: [PATCH 058/142] Revert "Strip off inlay hints data that should be resolved" Without holding all hints in host's cache, this is impossile. Currenly, we keep hint caches separate and isolated, so this will not work when we actually resolve. --- crates/lsp/src/lsp.rs | 4 +-- crates/project/src/lsp_command.rs | 56 +++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d8069c08b12e11b8b12155892dc974ae0d..78c858a90c46e2af349027ed3f4f361fe8805752 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,9 +434,7 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: Some(InlayHintResolveClientCapabilities { - properties: vec!["textEdits".to_string(), "tooltip".to_string()], - }), + resolve_support: None, dynamic_registration: Some(false), }), ..Default::default() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a8692257d8032fdca5667c2089249e806b241e34..08261b64f17c8714d1305fbff303080ddc82f0ab 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, - _: &mut AppContext, + cx: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response @@ -1963,17 +1963,51 @@ 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, - kind: response_hint.kind.map(|kind| kind.name().to_string()), - // 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(), - }, - )), + 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), + } }), }) .collect(), From 3434990b70cf79772d7f3f9d9f47a2b385fc2cb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 15:54:30 +0300 Subject: [PATCH 059/142] Store inlay hint resolve data --- crates/editor/src/display_map/inlay_map.rs | 6 +- crates/project/src/lsp_command.rs | 149 ++++++++++++++------- crates/project/src/project.rs | 16 +++ crates/rpc/proto/zed.proto | 16 +++ 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 9794ac45c1190ec88ccb471ee61630ec50d320ca..3cea513dea6c0c647d56afc9ff144b274dca350e 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1109,7 +1109,7 @@ mod tests { use super::*; use crate::{InlayId, MultiBuffer}; use gpui::AppContext; - use project::{InlayHint, InlayHintLabel}; + use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; @@ -1131,6 +1131,7 @@ mod tests { padding_right: false, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1151,6 +1152,7 @@ mod tests { padding_right: true, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1171,6 +1173,7 @@ mod tests { padding_right: false, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1191,6 +1194,7 @@ mod tests { padding_right: true, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f17c8714d1305fbff303080ddc82f0ab..d46ba4f5f71fec1f471882b4b8e4a05f6ebc1400 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,7 +1,7 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, - MarkupContent, Project, ProjectTransaction, + MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; @@ -1817,7 +1817,8 @@ impl LspCommand for InlayHints { server_id: LanguageServerId, mut cx: AsyncAppContext, ) -> Result> { - let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + let (lsp_adapter, lsp_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning // `const foo: boolean` into `const foo : boolean` which looks odd. // `rust-analyzer` does not have the padding for this case, and we have to accomodate both. @@ -1833,6 +1834,15 @@ impl LspCommand for InlayHints { .unwrap_or_default() .into_iter() .map(|lsp_hint| { + let resolve_state = match lsp_server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( + lsp::InlayHintOptions { + resolve_provider: Some(true), + .. + }, + ))) => ResolveState::CanResolve(lsp_hint.data), + _ => ResolveState::Resolved, + }; let kind = lsp_hint.kind.and_then(|kind| match kind { lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), @@ -1910,6 +1920,7 @@ impl LspCommand for InlayHints { }) } }), + resolve_state, } }) .collect()) @@ -1959,57 +1970,69 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| proto::InlayHint { - 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() - }) - } + .map(|response_hint| { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::CanResolve(resolve_data) => { + (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value })) + } + ResolveState::Resolved => (1, None), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, lsp_resolve_state + }); + proto::InlayHint { + 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, - }, - ) + 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), } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - }) + }), + resolve_state, + }}) .collect(), version: serialize_version(buffer_version), } @@ -2021,7 +2044,7 @@ impl LspCommand for InlayHints { project: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) @@ -2035,6 +2058,27 @@ impl LspCommand for InlayHints { .as_ref() .and_then(|location| location.buffer_id) .context("missing buffer id")?; + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!( + "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}", + ) + }); + + let lsp_resolve_state = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => ResolveState::CanResolve(lsp_resolve_state), + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; let hint = InlayHint { buffer_id, position: message_hint @@ -2103,6 +2147,7 @@ impl LspCommand for InlayHints { } }) }), + resolve_state, }; hints.push(hint); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 49074268f21a11cc3b57b43ae2e4409c749df6d2..1628234f98e077a92aae72c5ebd0a2c77b42e5c1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -342,6 +342,22 @@ pub struct InlayHint { pub padding_left: bool, pub padding_right: bool, pub tooltip: Option, + pub resolve_state: ResolveState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolveState { + Resolved, + CanResolve(Option), + Resolving, +} + +impl Hash for ResolveState { + fn hash(&self, state: &mut H) { + // Regular `lsp::LSPAny` is not hashable, so we can't hash it. + // LSP expects this data to not to change between requests, so we only hash the discriminant. + std::mem::discriminant(self).hash(state); + } } impl InlayHint { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f032ccce513de5feacae35e9980acb18c864d6c8..d0964064ab1e471f5150b53a4a23e96311e95798 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -754,6 +754,7 @@ message InlayHint { bool padding_left = 4; bool padding_right = 5; InlayHintTooltip tooltip = 6; + ResolveState resolve_state = 7; } message InlayHintLabel { @@ -787,6 +788,21 @@ message InlayHintLabelPartTooltip { } } +message ResolveState { + State state = 1; + LspResolveState lsp_resolve_state = 2; + + enum State { + Resolved = 0; + CanResolve = 1; + Resolving = 2; + } + + message LspResolveState { + string value = 1; + } +} + message RefreshInlayHints { uint64 project_id = 1; } From 80e871424194fb8c018d5d85d0d5182492a75238 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 21:09:04 +0300 Subject: [PATCH 060/142] Send inlay hint resolve requests --- crates/editor/src/display_map/inlay_map.rs | 4 - crates/editor/src/element.rs | 177 +++--- crates/editor/src/inlay_hint_cache.rs | 81 ++- crates/project/src/lsp_command.rs | 624 +++++++++++++-------- crates/project/src/project.rs | 140 ++++- crates/rpc/proto/zed.proto | 16 +- crates/rpc/src/proto.rs | 4 + crates/rpc/src/rpc.rs | 2 +- 8 files changed, 705 insertions(+), 343 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 3cea513dea6c0c647d56afc9ff144b274dca350e..026f1fc2c20a789319a4834dbd181bc61b7420ee 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1125,7 +1125,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: false, padding_right: false, @@ -1146,7 +1145,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: true, padding_right: true, @@ -1167,7 +1165,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: false, padding_right: false, @@ -1188,7 +1185,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: true, padding_right: true, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 57384195519f801c78b841608f772bbde1994782..2d2191e77fbacbcfaa2df13e79f4c7568e511a23 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -42,7 +42,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, ProjectPath, + InlayHintLabelPart, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -456,82 +456,21 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu + let mut go_to_definition_point = None; + let mut hover_at_point = None; if text_bounds.contains_point(position) { let point_for_position = position_map.point_for_position(text_bounds, position); if let Some(point) = point_for_position.as_valid() { - update_go_to_definition_link(editor, Some(point), cmd, shift, cx); - hover_at(editor, Some(point), cx); - return true; + go_to_definition_point = Some(point); + hover_at_point = Some(point); } else { - let hint_start_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { - Some(position_map.snapshot.display_point_to_inlay_offset( - point_for_position.exact_unclipped, - Bias::Left, - )) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) - } else { - None - }; - if let Some(hovered_offset) = hovered_offset { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let previous_valid_anchor = snapshot.anchor_at( - point_for_position - .previous_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Left, - ); - let next_valid_anchor = snapshot.anchor_at( - point_for_position - .next_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Right, - ); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| { - hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt() - }) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - if let Some(cached_hint) = editor - .inlay_hint_cache() - .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) - { - match &cached_hint.label { - project::InlayHintLabel::String(regular_label) => { - // TODO kb remove + check for tooltip for hover and resolve, if needed - eprintln!("regular string: {regular_label}"); - } - project::InlayHintLabel::LabelParts(label_parts) => { - if let Some(hovered_hint_part) = find_hovered_hint_part( - &label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) { - // TODO kb remove + check for tooltip and location and resolve, if needed - eprintln!("hint_part: {hovered_hint_part:?}"); - } - } - }; - } - } - } + (go_to_definition_point, hover_at_point) = + inlay_link_and_hover_points(position_map, point_for_position, editor, cx); } }; - update_go_to_definition_link(editor, None, cmd, shift, cx); - hover_at(editor, None, cx); + update_go_to_definition_link(editor, go_to_definition_point, cmd, shift, cx); + hover_at(editor, hover_at_point, cx); true } @@ -1876,6 +1815,104 @@ impl EditorElement { } } +fn inlay_link_and_hover_points( + position_map: &PositionMap, + point_for_position: PointForPosition, + editor: &mut Editor, + cx: &mut ViewContext<'_, '_, Editor>, +) -> (Option, Option) { + let hint_start_offset = position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some( + position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), + ) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + let mut go_to_definition_point = None; + let mut hover_at_point = None; + if let Some(hovered_offset) = hovered_offset { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_anchor = snapshot.anchor_at( + point_for_position + .previous_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Left, + ); + let next_valid_anchor = snapshot.anchor_at( + point_for_position + .next_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + if let Some(cached_hint) = + inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + previous_valid_anchor.excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + match &cached_hint.label { + project::InlayHintLabel::String(_) => { + if cached_hint.tooltip.is_some() { + dbg!(&cached_hint.tooltip); // TODO kb + // hover_at_point = Some(hovered_offset); + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some(hovered_hint_part) = find_hovered_hint_part( + &label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) { + if hovered_hint_part.tooltip.is_some() { + dbg!(&hovered_hint_part.tooltip); // TODO kb + // hover_at_point = Some(hovered_offset); + } + if let Some(location) = &hovered_hint_part.location { + dbg!(location); // TODO kb + // go_to_definition_point = Some(location); + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + } + + (go_to_definition_point, hover_at_point) +} + fn find_hovered_hint_part<'a>( label_parts: &'a [InlayHintLabelPart], hint_range: Range, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 5b9bdd08ec25d1d2c872fa74bce9d6ab865d2781..52a4039a76e6b31dd9e46f13de627591e5b916f8 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -13,7 +13,7 @@ use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; use parking_lot::RwLock; -use project::InlayHint; +use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; @@ -60,7 +60,7 @@ struct ExcerptHintsUpdate { excerpt_id: ExcerptId, remove_from_visible: Vec, remove_from_cache: HashSet, - add_to_cache: HashSet, + add_to_cache: Vec, } #[derive(Debug, Clone, Copy)] @@ -409,6 +409,79 @@ impl InlayHintCache { pub fn version(&self) -> usize { self.version } + + pub fn spawn_hint_resolve( + &self, + buffer_id: u64, + excerpt_id: ExcerptId, + id: InlayId, + cx: &mut ViewContext<'_, '_, Editor>, + ) { + if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn(|editor, mut cx| async move { + let resolved_hint_task = editor.update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .buffer(buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })) + }) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + if let Some(mut resolved_hint) = + resolved_hint_task.await.context("hint resolve task")? + { + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if cached_hint.resolve_state == ResolveState::Resolving + { + resolved_hint.resolve_state = + ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } + })?; + } + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + } + } } fn spawn_new_update_tasks( @@ -632,7 +705,7 @@ fn calculate_hint_updates( cached_excerpt_hints: Option>>, visible_hints: &[Inlay], ) -> Option { - let mut add_to_cache: HashSet = HashSet::default(); + let mut add_to_cache = Vec::::new(); let mut excerpt_hints_to_persist = HashMap::default(); for new_hint in new_excerpt_hints { if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) { @@ -659,7 +732,7 @@ fn calculate_hint_updates( None => true, }; if missing_from_cache { - add_to_cache.insert(new_hint); + add_to_cache.push(new_hint); } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d46ba4f5f71fec1f471882b4b8e4a05f6ebc1400..7b4d689a81142857ee5153028f7521a65f7388c8 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,6 +1,6 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, - InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Item, Location, LocationLink, MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; @@ -12,8 +12,9 @@ use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, - Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1776,6 +1777,371 @@ impl LspCommand for OnTypeFormatting { } } +impl InlayHints { + pub fn lsp_to_project_hint( + lsp_hint: lsp::InlayHint, + buffer_handle: &ModelHandle, + resolve_state: ResolveState, + force_no_type_left_padding: bool, + cx: &AppContext, + ) -> InlayHint { + let kind = lsp_hint.kind.and_then(|kind| match kind { + lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), + lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), + _ => None, + }); + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { + false + } else { + lsp_hint.padding_left.unwrap_or(false) + }; + InlayHint { + position: if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + }, + padding_left, + padding_right: lsp_hint.padding_right.unwrap_or(false), + label: match lsp_hint.label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => InlayHintLabel::LabelParts( + lsp_parts + .into_iter() + .map(|label_part| InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location: label_part.location.map(|lsp_location| { + let target_start = buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + Location { + buffer: buffer_handle.clone(), + range: buffer.anchor_after(target_start) + ..buffer.anchor_before(target_end), + } + }), + }) + .collect(), + ), + }, + kind, + tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), + lsp::InlayHintTooltip::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + resolve_state, + } + } + + pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::CanResolve(server_id, resolve_data) => ( + 0, + resolve_data + .map(|json_data| { + serde_json::to_string(&json_data) + .expect("failed to serialize resolve json data") + }) + .map(|value| proto::resolve_state::LspResolveState { + server_id: server_id.0 as u64, + value, + }), + ), + ResolveState::Resolved => (1, None), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, + lsp_resolve_state, + }); + proto::InlayHint { + 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 { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + 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 { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } + }), + resolve_state, + } + } + + pub async fn proto_to_project_hint( + message_hint: proto::InlayHint, + project: &ModelHandle, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let buffer_id = message_hint + .position + .as_ref() + .and_then(|location| location.buffer_id) + .context("missing buffer id")?; + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) + }); + let resolve_state_data = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::>(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state)) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => { + let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| { + format!( + "No lsp resolve data for the hint that can be resolved: {message_hint:?}" + ) + })?; + ResolveState::CanResolve(server_id, lsp_resolve_state) + } + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; + Ok(InlayHint { + position: message_hint + .position + .and_then(language::proto::deserialize_anchor) + .context("invalid position")?, + label: match message_hint + .label + .and_then(|label| label.label) + .context("missing label")? + { + proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), + proto::inlay_hint_label::Label::LabelParts(parts) => { + let mut label_parts = Vec::new(); + for part in parts.parts { + let buffer = project + .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx)) + .await?; + label_parts.push(InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.map(|tooltip| match tooltip.content { + Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => { + InlayHintLabelPartTooltip::String(s) + } + Some( + proto::inlay_hint_label_part_tooltip::Content::MarkupContent( + markup_content, + ), + ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }), + None => InlayHintLabelPartTooltip::String(String::new()), + }), + location: match part.location { + Some(location) => Some(Location { + range: location + .start + .and_then(language::proto::deserialize_anchor) + .context("invalid start")? + ..location + .end + .and_then(language::proto::deserialize_anchor) + .context("invalid end")?, + buffer, + }), + None => None, + }, + }); + } + + InlayHintLabel::LabelParts(label_parts) + } + }, + padding_left: message_hint.padding_left, + padding_right: message_hint.padding_right, + kind: message_hint + .kind + .as_deref() + .and_then(InlayHintKind::from_name), + tooltip: message_hint.tooltip.and_then(|tooltip| { + Some(match tooltip.content? { + proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), + proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }) + } + }) + }), + resolve_state, + }) + } + + // TODO kb instead, store all LSP data inside the project::InlayHint? + pub fn project_to_lsp_hint( + hint: InlayHint, + project: &ModelHandle, + snapshot: &BufferSnapshot, + cx: &AsyncAppContext, + ) -> lsp::InlayHint { + lsp::InlayHint { + position: point_to_lsp(hint.position.to_point_utf16(snapshot)), + kind: hint.kind.map(|kind| match kind { + InlayHintKind::Type => lsp::InlayHintKind::TYPE, + InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER, + }), + text_edits: None, + tooltip: hint.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s), + InlayHintTooltip::MarkupContent(markup_content) => { + lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => lsp::MarkupKind::PlainText, + HoverBlockKind::Markdown => lsp::MarkupKind::Markdown, + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }) + } + }) + }), + label: match hint.label { + InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s), + InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts( + label_parts + .into_iter() + .map(|part| lsp::InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintLabelPartTooltip::String(s) => { + lsp::InlayHintLabelPartTooltip::String(s) + } + InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => { + lsp::MarkupKind::PlainText + } + HoverBlockKind::Markdown => { + lsp::MarkupKind::Markdown + } + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }, + ) + } + }) + }), + location: part.location.and_then(|location| { + let path = cx.read(|cx| { + let project_path = location.buffer.read(cx).project_path(cx)?; + project.read(cx).absolute_path(&project_path, cx) + })?; + Some(lsp::Location::new( + lsp::Url::from_file_path(path).unwrap(), + range_to_lsp( + location.range.start.to_point_utf16(snapshot) + ..location.range.end.to_point_utf16(snapshot), + ), + )) + }), + command: None, + }) + .collect(), + ), + }, + padding_left: Some(hint.padding_left), + padding_right: Some(hint.padding_right), + data: match hint.resolve_state { + ResolveState::CanResolve(_, data) => data, + ResolveState::Resolving | ResolveState::Resolved => None, + }, + } + } +} + #[async_trait(?Send)] impl LspCommand for InlayHints { type Response = Vec; @@ -1829,7 +2195,6 @@ impl LspCommand for InlayHints { let force_no_type_left_padding = lsp_adapter.name.0.as_ref() == "typescript-language-server"; cx.read(|cx| { - let origin_buffer = buffer.read(cx); Ok(message .unwrap_or_default() .into_iter() @@ -1840,88 +2205,18 @@ impl LspCommand for InlayHints { resolve_provider: Some(true), .. }, - ))) => ResolveState::CanResolve(lsp_hint.data), + ))) => { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } _ => ResolveState::Resolved, }; - let kind = lsp_hint.kind.and_then(|kind| match kind { - lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), - lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), - _ => None, - }); - let position = origin_buffer - .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); - let padding_left = - if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { - false - } else { - lsp_hint.padding_left.unwrap_or(false) - }; - InlayHint { - buffer_id: origin_buffer.remote_id(), - position: if kind == Some(InlayHintKind::Parameter) { - origin_buffer.anchor_before(position) - } else { - origin_buffer.anchor_after(position) - }, - padding_left, - padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => { - InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map( - |tooltip| { - match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent( - markup_content, - ) => InlayHintLabelPartTooltip::MarkupContent( - MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }, - ), - } - }, - ), - location: label_part.location.map(|lsp_location| { - let target_start = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(target_start) - ..origin_buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ) - } - }, - kind, - tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), - lsp::InlayHintTooltip::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }) - } - }), + InlayHints::lsp_to_project_hint( + lsp_hint, + &buffer, resolve_state, - } + force_no_type_left_padding, + cx, + ) }) .collect()) }) @@ -1970,69 +2265,7 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| { - let (state, lsp_resolve_state) = match response_hint.resolve_state { - ResolveState::CanResolve(resolve_data) => { - (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value })) - } - ResolveState::Resolved => (1, None), - ResolveState::Resolving => (2, None), - }; - let resolve_state = Some(proto::ResolveState { - state, lsp_resolve_state - }); - proto::InlayHint { - 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), - } - }), - resolve_state, - }}) + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint, cx)) .collect(), version: serialize_version(buffer_version), } @@ -2053,104 +2286,7 @@ impl LspCommand for InlayHints { let mut hints = Vec::new(); for message_hint in message.hints { - let buffer_id = message_hint - .position - .as_ref() - .and_then(|location| location.buffer_id) - .context("missing buffer id")?; - let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { - panic!( - "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}", - ) - }); - - let lsp_resolve_state = resolve_state - .lsp_resolve_state.as_ref() - .map(|lsp_resolve_state| { - serde_json::from_str::(&lsp_resolve_state.value) - .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) - }) - .transpose()?; - let resolve_state = match resolve_state.state { - 0 => ResolveState::Resolved, - 1 => ResolveState::CanResolve(lsp_resolve_state), - 2 => ResolveState::Resolving, - invalid => { - anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") - } - }; - let hint = InlayHint { - buffer_id, - position: message_hint - .position - .and_then(language::proto::deserialize_anchor) - .context("invalid position")?, - label: match message_hint - .label - .and_then(|label| label.label) - .context("missing label")? - { - proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), - proto::inlay_hint_label::Label::LabelParts(parts) => { - let mut label_parts = Vec::new(); - for part in parts.parts { - label_parts.push(InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.map(|tooltip| match tooltip.content { - Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s), - Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - None => InlayHintLabelPartTooltip::String(String::new()), - }), - location: match part.location { - Some(location) => { - let target_buffer = project - .update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(location.buffer_id, cx) - }) - .await?; - Some(Location { - range: location - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")? - ..location - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?, - buffer: target_buffer, - })}, - None => None, - }, - }); - } - - InlayHintLabel::LabelParts(label_parts) - } - }, - padding_left: message_hint.padding_left, - padding_right: message_hint.padding_right, - kind: message_hint - .kind - .as_deref() - .and_then(InlayHintKind::from_name), - tooltip: message_hint.tooltip.and_then(|tooltip| { - Some(match tooltip.content? { - proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), - proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }) - } - }) - }), - resolve_state, - }; - - hints.push(hint); + hints.push(InlayHints::proto_to_project_hint(message_hint, &project, &mut cx).await?); } Ok(hints) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1628234f98e077a92aae72c5ebd0a2c77b42e5c1..65be7cceb10ef3a1fea39ba908d239b3948085bf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -333,9 +333,8 @@ pub struct Location { pub range: Range, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { - pub buffer_id: u64, pub position: language::Anchor, pub label: InlayHintLabel, pub kind: Option, @@ -348,18 +347,10 @@ pub struct InlayHint { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolveState { Resolved, - CanResolve(Option), + CanResolve(LanguageServerId, Option), Resolving, } -impl Hash for ResolveState { - fn hash(&self, state: &mut H) { - // Regular `lsp::LSPAny` is not hashable, so we can't hash it. - // LSP expects this data to not to change between requests, so we only hash the discriminant. - std::mem::discriminant(self).hash(state); - } -} - impl InlayHint { pub fn text(&self) -> String { match &self.label { @@ -369,34 +360,34 @@ impl InlayHint { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabel { String(String), LabelParts(Vec), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, pub location: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabelPartTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MarkupContent { - pub kind: String, + pub kind: HoverBlockKind, pub value: String, } @@ -430,7 +421,7 @@ pub struct HoverBlock { pub kind: HoverBlockKind, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum HoverBlockKind { PlainText, Markdown, @@ -567,6 +558,7 @@ impl Project { client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_inlay_hint); client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_synchronize_buffers); @@ -4985,7 +4977,7 @@ impl Project { buffer_handle: ModelHandle, range: Range, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); let range_start = range.start; @@ -5035,6 +5027,79 @@ impl Project { } } + pub fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: ModelHandle, + server_id: LanguageServerId, + cx: &mut ModelContext, + ) -> Task>> { + if self.is_local() { + let buffer = buffer_handle.read(cx); + let (_, lang_server) = if let Some((adapter, server)) = + self.language_server_for_buffer(buffer, server_id, cx) + { + (adapter.clone(), server.clone()) + } else { + return Task::ready(Ok(None)); + }; + let can_resolve = lang_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return Task::ready(Ok(None)); + } + + let buffer_snapshot = buffer.snapshot(); + cx.spawn(|project, cx| async move { + let resolve_task = lang_server.request::( + InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), + ); + let resolved_hint = resolve_task + .await + .context("inlay hint resolve LSP request")?; + let resolved_hint = cx.read(|cx| { + InlayHints::lsp_to_project_hint( + resolved_hint, + &buffer_handle, + ResolveState::Resolved, + false, + cx, + ) + }); + Ok(Some(resolved_hint)) + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::ResolveInlayHint { + project_id, + buffer_id: buffer_handle.read(cx).remote_id(), + language_server_id: server_id.0 as u64, + hint: Some(InlayHints::project_to_proto_hint(hint, cx)), + }; + cx.spawn(|project, mut cx| async move { + let response = client + .request(request) + .await + .context("inlay hints proto request")?; + match response.hint { + Some(resolved_hint) => { + InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) + .await + .map(Some) + .context("inlay hints proto response conversion") + } + None => Ok(None), + } + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + #[allow(clippy::type_complexity)] pub fn search( &self, @@ -6832,6 +6897,43 @@ impl Project { })) } + async fn handle_resolve_inlay_hint( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let proto_hint = envelope + .payload + .hint + .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); + let hint = InlayHints::proto_to_project_hint(proto_hint, &this, &mut cx) + .await + .context("resolved proto inlay hint conversion")?; + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade(cx)) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) + })?; + let resolved_hint = this + .update(&mut cx, |project, cx| { + project.resolve_inlay_hint( + hint, + buffer, + LanguageServerId(envelope.payload.language_server_id as usize), + cx, + ) + }) + .await + .context("inlay hints fetch")? + .map(|hint| cx.read(|cx| InlayHints::project_to_proto_hint(hint, cx))); + + Ok(proto::ResolveInlayHintResponse { + hint: resolved_hint, + }) + } + async fn handle_refresh_inlay_hints( this: ModelHandle, _: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d0964064ab1e471f5150b53a4a23e96311e95798..dbd700e264af7c1c3f89b014bbf9ad9476fd5536 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,6 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; + ResolveInlayHint resolve_inlay_hint = 131; + ResolveInlayHintResponse resolve_inlay_hint_response = 132; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; @@ -800,15 +802,27 @@ message ResolveState { message LspResolveState { string value = 1; + uint64 server_id = 2; } } +message ResolveInlayHint { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; + InlayHint hint = 4; +} + +message ResolveInlayHintResponse { + InlayHint hint = 1; +} + message RefreshInlayHints { uint64 project_id = 1; } message MarkupContent { - string kind = 1; + bool is_markdown = 1; string value = 2; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0f49c6230c0229c2067c9c8fcd49ba9bf850795..2e4dce01e1a3bf5789206c80b3a4574f6e198c0d 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -197,6 +197,8 @@ messages!( (OnTypeFormattingResponse, Background), (InlayHints, Background), (InlayHintsResponse, Background), + (ResolveInlayHint, Background), + (ResolveInlayHintResponse, Background), (RefreshInlayHints, Foreground), (Ping, Foreground), (PrepareRename, Background), @@ -299,6 +301,7 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), (InlayHints, InlayHintsResponse), + (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), @@ -355,6 +358,7 @@ entity_messages!( PerformRename, OnTypeFormatting, InlayHints, + ResolveInlayHint, RefreshInlayHints, PrepareRename, ReloadBuffers, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 3cb8b6bffa2ca1549ca854db39e46ef8fc8634a7..bc9dd6f80ba039bb705e3d1518c737ba56c969b9 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 60; +pub const PROTOCOL_VERSION: u32 = 61; From ac86bbac75b7f0347b9f1619f247c388bf8aea19 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 16:25:01 +0300 Subject: [PATCH 061/142] Prepare for hover functionality refactoring --- crates/editor/src/element.rs | 37 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2d2191e77fbacbcfaa2df13e79f4c7568e511a23..405a6e1a08348dbeb78b60c106d6e3080f9f81dd 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -456,21 +456,27 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let mut go_to_definition_point = None; - let mut hover_at_point = None; if text_bounds.contains_point(position) { let point_for_position = position_map.point_for_position(text_bounds, position); - if let Some(point) = point_for_position.as_valid() { - go_to_definition_point = Some(point); - hover_at_point = Some(point); - } else { - (go_to_definition_point, hover_at_point) = - inlay_link_and_hover_points(position_map, point_for_position, editor, cx); + match point_for_position.as_valid() { + Some(point) => { + update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + hover_at(editor, Some(point), cx); + } + None => { + update_inlay_link_and_hover_points( + position_map, + point_for_position, + editor, + cx, + ); + } } - }; + } else { + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); + } - update_go_to_definition_link(editor, go_to_definition_point, cmd, shift, cx); - hover_at(editor, hover_at_point, cx); true } @@ -1815,12 +1821,13 @@ impl EditorElement { } } -fn inlay_link_and_hover_points( +// TODO kb implement +fn update_inlay_link_and_hover_points( position_map: &PositionMap, point_for_position: PointForPosition, editor: &mut Editor, cx: &mut ViewContext<'_, '_, Editor>, -) -> (Option, Option) { +) { let hint_start_offset = position_map .snapshot .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); @@ -1839,8 +1846,6 @@ fn inlay_link_and_hover_points( } else { None }; - let mut go_to_definition_point = None; - let mut hover_at_point = None; if let Some(hovered_offset) = hovered_offset { let buffer = editor.buffer().read(cx); let snapshot = buffer.snapshot(cx); @@ -1909,8 +1914,6 @@ fn inlay_link_and_hover_points( } } } - - (go_to_definition_point, hover_at_point) } fn find_hovered_hint_part<'a>( From 7eab18ec897738b7e6e5a8b71f15bcc3a3af4fe5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 17:54:34 +0300 Subject: [PATCH 062/142] Pass inlay go to definition data --- crates/editor/src/editor.rs | 13 +- crates/editor/src/element.rs | 82 ++++++-- crates/editor/src/items.rs | 2 +- crates/editor/src/link_go_to_definition.rs | 216 +++++++++++++++------ 4 files changed, 229 insertions(+), 84 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 775f3c07ece735c781cd60f9600bf7027320d25d..fb1e87580aa8bed7b994e8c8bc92659ffd096d71 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,9 +64,7 @@ use language::{ Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::{ - hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, -}; +use link_go_to_definition::{hide_link_definition, show_link_definition, LinkGoToDefinitionState}; use log::error; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -8307,14 +8305,11 @@ impl View for Editor { ) -> bool { let pending_selection = self.has_pending_selection(); - if let Some(point) = self.link_go_to_definition_state.last_mouse_location.clone() { + if let Some(point) = &self.link_go_to_definition_state.last_trigger_point { if event.cmd && !pending_selection { + let point = point.clone(); let snapshot = self.snapshot(cx); - let kind = if event.shift { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; + let kind = point.definition_kind(event.shift); show_link_definition(kind, self, point, snapshot, cx); return false; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 405a6e1a08348dbeb78b60c106d6e3080f9f81dd..15b28914af059cbd238973ae4c5ba359aee32b35 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,6 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, + GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -42,7 +43,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, ProjectPath, ResolveState, + InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -395,7 +396,15 @@ impl EditorElement { None }; - update_go_to_definition_link(editor, point, cmd, shift, cx); + update_go_to_definition_link( + editor, + point + .map(GoToDefinitionTrigger::Text) + .unwrap_or(GoToDefinitionTrigger::None), + cmd, + shift, + cx, + ); if editor.has_pending_selection() { let mut scroll_delta = Vector2F::zero(); @@ -460,7 +469,13 @@ impl EditorElement { let point_for_position = position_map.point_for_position(text_bounds, position); match point_for_position.as_valid() { Some(point) => { - update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(point), + cmd, + shift, + cx, + ); hover_at(editor, Some(point), cx); } None => { @@ -468,12 +483,13 @@ impl EditorElement { position_map, point_for_position, editor, + (cmd, shift), cx, ); } } } else { - update_go_to_definition_link(editor, None, cmd, shift, cx); + update_go_to_definition_link(editor, GoToDefinitionTrigger::None, cmd, shift, cx); hover_at(editor, None, cx); } @@ -1821,11 +1837,11 @@ impl EditorElement { } } -// TODO kb implement fn update_inlay_link_and_hover_points( position_map: &PositionMap, point_for_position: PointForPosition, editor: &mut Editor, + (cmd_held, shift_held): (bool, bool), cx: &mut ViewContext<'_, '_, Editor>, ) { let hint_start_offset = position_map @@ -1861,6 +1877,9 @@ fn update_inlay_link_and_hover_points( .to_point(&position_map.snapshot.display_snapshot), Bias::Right, ); + + let mut go_to_definition_updated = false; + let mut hover_updated = false; if let Some(hovered_hint) = editor .visible_inlay_hints(cx) .into_iter() @@ -1872,7 +1891,7 @@ fn update_inlay_link_and_hover_points( if let Some(cached_hint) = inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) { - match &cached_hint.resolve_state { + match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( @@ -1884,7 +1903,7 @@ fn update_inlay_link_and_hover_points( } } ResolveState::Resolved => { - match &cached_hint.label { + match cached_hint.label { project::InlayHintLabel::String(_) => { if cached_hint.tooltip.is_some() { dbg!(&cached_hint.tooltip); // TODO kb @@ -1893,7 +1912,7 @@ fn update_inlay_link_and_hover_points( } project::InlayHintLabel::LabelParts(label_parts) => { if let Some(hovered_hint_part) = find_hovered_hint_part( - &label_parts, + label_parts, hint_start_offset..hint_end_offset, hovered_offset, ) { @@ -1901,9 +1920,31 @@ fn update_inlay_link_and_hover_points( dbg!(&hovered_hint_part.tooltip); // TODO kb // hover_at_point = Some(hovered_offset); } - if let Some(location) = &hovered_hint_part.location { - dbg!(location); // TODO kb - // go_to_definition_point = Some(location); + if let Some(location) = hovered_hint_part.location { + if let Some(buffer) = cached_hint + .position + .buffer_id + .and_then(|buffer_id| buffer.buffer(buffer_id)) + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + hovered_hint.position, + LocationLink { + origin: Some(Location { + buffer, + range: cached_hint.position + ..cached_hint.position, + }), + target: location, + }, + ), + cmd_held, + shift_held, + cx, + ); + } } } } @@ -1913,14 +1954,27 @@ fn update_inlay_link_and_hover_points( } } } + + if !go_to_definition_updated { + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::None, + cmd_held, + shift_held, + cx, + ); + } + if !hover_updated { + hover_at(editor, None, cx); + } } } -fn find_hovered_hint_part<'a>( - label_parts: &'a [InlayHintLabelPart], +fn find_hovered_hint_part( + label_parts: Vec, hint_range: Range, hovered_offset: InlayOffset, -) -> Option<&'a InlayHintLabelPart> { +) -> Option { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; for part in label_parts { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 477eab41ac9cc7a0c7b38e0ec07f3eb41f46963e..30ed56af476547503d95afb563635ad9bddcbec6 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -615,7 +615,7 @@ impl Item for Editor { fn workspace_deactivated(&mut self, cx: &mut ViewContext) { hide_link_definition(self, cx); - self.link_go_to_definition_state.last_mouse_location = None; + self.link_go_to_definition_state.last_trigger_point = None; } fn is_dirty(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 31df11a01959e4795738658d813ce4b23bfcafe8..a0014337277905d347b42487e9194577ce4e81c4 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -7,16 +7,50 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_mouse_location: Option, + pub last_trigger_point: Option, pub symbol_range: Option>, pub kind: Option, pub definitions: Vec, pub task: Option>>, } +pub enum GoToDefinitionTrigger { + Text(DisplayPoint), + InlayHint(Anchor, LocationLink), + None, +} + +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(Anchor, LocationLink), +} + +impl TriggerPoint { + fn anchor(&self) -> &Anchor { + match self { + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(anchor, _) => anchor, + } + } + + pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { + match self { + TriggerPoint::Text(_) => { + if shift { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + } + } + TriggerPoint::InlayHint(_, link) => LinkDefinitionKind::Type, + } + } +} + pub fn update_go_to_definition_link( editor: &mut Editor, - point: Option, + origin: GoToDefinitionTrigger, cmd_held: bool, shift_held: bool, cx: &mut ViewContext, @@ -25,23 +59,30 @@ pub fn update_go_to_definition_link( // Store new mouse point as an anchor let snapshot = editor.snapshot(cx); - let point = point.map(|point| { - snapshot - .buffer_snapshot - .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)) - }); + let trigger_point = match origin { + GoToDefinitionTrigger::Text(p) => { + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + p.to_offset(&snapshot.display_snapshot, Bias::Left), + ))) + } + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::None => None, + }; // If the new point is the same as the previously stored one, return early if let (Some(a), Some(b)) = ( - &point, - &editor.link_go_to_definition_state.last_mouse_location, + &trigger_point, + &editor.link_go_to_definition_state.last_trigger_point, ) { - if a.cmp(b, &snapshot.buffer_snapshot).is_eq() { + if a.anchor() + .cmp(b.anchor(), &snapshot.buffer_snapshot) + .is_eq() + { return; } } - editor.link_go_to_definition_state.last_mouse_location = point.clone(); + editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone(); if pending_nonempty_selection { hide_link_definition(editor, cx); @@ -49,14 +90,9 @@ pub fn update_go_to_definition_link( } if cmd_held { - if let Some(point) = point { - let kind = if shift_held { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; - - show_link_definition(kind, editor, point, snapshot, cx); + if let Some(trigger_point) = trigger_point { + let kind = trigger_point.definition_kind(shift_held); + show_link_definition(kind, editor, trigger_point, snapshot, cx); return; } } @@ -73,7 +109,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: Anchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -86,10 +122,11 @@ pub fn show_link_definition( return; } + let trigger_anchor = trigger_point.anchor().clone(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) - .text_anchor_for_position(trigger_point.clone(), cx) + .text_anchor_for_position(trigger_anchor.clone(), cx) { output } else { @@ -99,7 +136,7 @@ pub fn show_link_definition( let excerpt_id = if let Some((excerpt_id, _, _)) = editor .buffer() .read(cx) - .excerpt_containing(trigger_point.clone(), cx) + .excerpt_containing(trigger_anchor.clone(), cx) { excerpt_id } else { @@ -116,12 +153,12 @@ pub fn show_link_definition( if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { let point_after_start = symbol_range .start - .cmp(&trigger_point, &snapshot.buffer_snapshot) + .cmp(&trigger_anchor, &snapshot.buffer_snapshot) .is_le(); let point_before_end = symbol_range .end - .cmp(&trigger_point, &snapshot.buffer_snapshot) + .cmp(&trigger_anchor, &snapshot.buffer_snapshot) .is_ge(); let point_within_range = point_after_start && point_before_end; @@ -132,34 +169,45 @@ pub fn show_link_definition( let task = cx.spawn(|this, mut cx| { async move { - // query the LSP for definition info - let definition_request = cx.update(|cx| { - project.update(cx, |project, cx| match definition_kind { - LinkDefinitionKind::Symbol => project.definition(&buffer, buffer_position, cx), - - LinkDefinitionKind::Type => { - project.type_definition(&buffer, buffer_position, cx) - } - }) - }); + let result = match trigger_point { + TriggerPoint::Text(_) => { + // query the LSP for definition info + cx.update(|cx| { + project.update(cx, |project, cx| match definition_kind { + LinkDefinitionKind::Symbol => { + project.definition(&buffer, buffer_position, cx) + } - let result = definition_request.await.ok().map(|definition_result| { - ( - definition_result.iter().find_map(|link| { - link.origin.as_ref().map(|origin| { - let start = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); - let end = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - - start..end + LinkDefinitionKind::Type => { + project.type_definition(&buffer, buffer_position, cx) + } }) - }), - definition_result, - ) - }); + }) + .await + .ok() + .map(|definition_result| { + ( + definition_result.iter().find_map(|link| { + link.origin.as_ref().map(|origin| { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); + + start..end + }) + }), + definition_result, + ) + }) + } + TriggerPoint::InlayHint(trigger_source, trigger_target) => { + // TODO kb range is wrong, should be in inlay coordinates + Some((Some(trigger_source..trigger_source), vec![trigger_target])) + } + }; this.update(&mut cx, |this, cx| { // Clear any existing highlights @@ -202,7 +250,7 @@ pub fn show_link_definition( // If no symbol range returned from language server, use the surrounding word. let highlight_range = symbol_range.unwrap_or_else(|| { let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_point); + let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end) @@ -355,7 +403,13 @@ mod tests { // Press cmd+shift to trigger highlight cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, true, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + true, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -461,7 +515,13 @@ mod tests { }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -482,7 +542,7 @@ mod tests { "}); // Response without source range still highlights word - cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None); + cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); let mut requests = cx.handle_request::(move |url, _, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ lsp::LocationLink { @@ -495,7 +555,13 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -517,7 +583,13 @@ mod tests { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -534,7 +606,13 @@ mod tests { fn do_work() { teˇst(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), false, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + false, + false, + cx, + ); }); cx.foreground().run_until_parked(); @@ -593,7 +671,13 @@ mod tests { // Moving the mouse restores the highlights. cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -607,7 +691,13 @@ mod tests { fn do_work() { tesˇt(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -703,7 +793,13 @@ mod tests { }); }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); assert!(requests.try_next().is_err()); From 477fc865f5b6f2b1bf43cb4350f49eced80e4e29 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 20:21:38 +0300 Subject: [PATCH 063/142] Properly resolve inlay label parts' locations and buffers --- crates/editor/src/display_map/inlay_map.rs | 1 + crates/editor/src/element.rs | 33 +-- crates/editor/src/link_go_to_definition.rs | 43 ++-- crates/project/src/lsp_command.rs | 256 ++++++++++++++------- crates/project/src/project.rs | 21 +- 5 files changed, 222 insertions(+), 132 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 026f1fc2c20a789319a4834dbd181bc61b7420ee..34181a576932d2aecc2cda538297fda365df4726 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1010,6 +1010,7 @@ impl InlaySnapshot { }) { Ok(i) | Err(i) => i, }; + // TODO kb add a way to highlight inlay hints through here. for range in &ranges[start_ix..] { if range.start.cmp(&transform_end, &self.buffer).is_ge() { break; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 15b28914af059cbd238973ae4c5ba359aee32b35..c28f14d98a916ab93a57be52fb20f69a487ee2ad 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -358,18 +358,15 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - if let Some(point) = position_map - .point_for_position(text_bounds, position) - .as_valid() - { - if shift { - go_to_fetched_type_definition(editor, point, alt, cx); - } else { - go_to_fetched_definition(editor, point, alt, cx); - } - - return true; + let point = position_map.point_for_position(text_bounds, position); + let could_be_inlay = point.as_valid().is_none(); + if shift || could_be_inlay { + go_to_fetched_type_definition(editor, point, alt, cx); + } else { + go_to_fetched_definition(editor, point, alt, cx); } + + return true; } end_selection @@ -2818,14 +2815,24 @@ struct PositionMap { } #[derive(Debug)] -struct PointForPosition { +pub struct PointForPosition { previous_valid: DisplayPoint, - next_valid: DisplayPoint, + pub next_valid: DisplayPoint, exact_unclipped: DisplayPoint, column_overshoot_after_line_end: u32, } impl PointForPosition { + #[cfg(test)] + pub fn valid(valid: DisplayPoint) -> Self { + Self { + previous_valid: valid, + next_valid: valid, + exact_unclipped: valid, + column_overshoot_after_line_end: 0, + } + } + fn as_valid(&self) -> Option { if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { Some(self.previous_valid) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index a0014337277905d347b42487e9194577ce4e81c4..b043af613279e44068d483c25ec17a2f752354dd 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,4 +1,4 @@ -use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{element::PointForPosition, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; @@ -7,7 +7,7 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_trigger_point: Option, + pub last_trigger_point: Option, pub symbol_range: Option>, pub kind: Option, pub definitions: Vec, @@ -21,29 +21,29 @@ pub enum GoToDefinitionTrigger { } #[derive(Debug, Clone)] -pub enum TriggerPoint { +pub enum TriggerAnchor { Text(Anchor), InlayHint(Anchor, LocationLink), } -impl TriggerPoint { +impl TriggerAnchor { fn anchor(&self) -> &Anchor { match self { - TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(anchor, _) => anchor, + TriggerAnchor::Text(anchor) => anchor, + TriggerAnchor::InlayHint(anchor, _) => anchor, } } pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { - TriggerPoint::Text(_) => { + TriggerAnchor::Text(_) => { if shift { LinkDefinitionKind::Type } else { LinkDefinitionKind::Symbol } } - TriggerPoint::InlayHint(_, link) => LinkDefinitionKind::Type, + TriggerAnchor::InlayHint(_, _) => LinkDefinitionKind::Type, } } } @@ -61,11 +61,11 @@ pub fn update_go_to_definition_link( let snapshot = editor.snapshot(cx); let trigger_point = match origin { GoToDefinitionTrigger::Text(p) => { - Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + Some(TriggerAnchor::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerAnchor::InlayHint(p, target)), GoToDefinitionTrigger::None => None, }; @@ -109,7 +109,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: TriggerPoint, + trigger_point: TriggerAnchor, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -170,7 +170,7 @@ pub fn show_link_definition( let task = cx.spawn(|this, mut cx| { async move { let result = match trigger_point { - TriggerPoint::Text(_) => { + TriggerAnchor::Text(_) => { // query the LSP for definition info cx.update(|cx| { project.update(cx, |project, cx| match definition_kind { @@ -203,8 +203,9 @@ pub fn show_link_definition( ) }) } - TriggerPoint::InlayHint(trigger_source, trigger_target) => { - // TODO kb range is wrong, should be in inlay coordinates + TriggerAnchor::InlayHint(trigger_source, trigger_target) => { + // TODO kb range is wrong, should be in inlay coordinates have a proper inlay range. + // Or highlight inlays differently, in their layer? Some((Some(trigger_source..trigger_source), vec![trigger_target])) } }; @@ -293,7 +294,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { pub fn go_to_fetched_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -302,7 +303,7 @@ pub fn go_to_fetched_definition( pub fn go_to_fetched_type_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -312,7 +313,7 @@ pub fn go_to_fetched_type_definition( fn go_to_fetched_definition_of_kind( kind: LinkDefinitionKind, editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -330,7 +331,7 @@ fn go_to_fetched_definition_of_kind( } else { editor.select( SelectPhase::Begin { - position: point, + position: point.next_valid, add: false, click_count: 1, }, @@ -460,7 +461,7 @@ mod tests { }); cx.update_editor(|editor, cx| { - go_to_fetched_type_definition(editor, hover_point, false, cx); + go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); @@ -707,7 +708,7 @@ mod tests { // Cmd click with existing definition doesn't re-request and dismisses highlight cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); // Assert selection moved to to definition cx.lsp @@ -748,7 +749,7 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 7b4d689a81142857ee5153028f7521a65f7388c8..20bb302b5b8963e996a1da47564f26ad191b56ad 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -7,14 +7,15 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; use fs::LineEnding; +use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, - CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, - Unclipped, + CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, + Transaction, Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1432,7 +1433,7 @@ impl LspCommand for GetCompletions { }) }); - Ok(futures::future::join_all(completions).await) + Ok(future::join_all(completions).await) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { @@ -1500,7 +1501,7 @@ impl LspCommand for GetCompletions { let completions = message.completions.into_iter().map(|completion| { language::proto::deserialize_completion(completion, language.clone()) }); - futures::future::try_join_all(completions).await + future::try_join_all(completions).await } fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 { @@ -1778,73 +1779,50 @@ impl LspCommand for OnTypeFormatting { } impl InlayHints { - pub fn lsp_to_project_hint( + pub async fn lsp_to_project_hint( lsp_hint: lsp::InlayHint, + project: &ModelHandle, buffer_handle: &ModelHandle, + server_id: LanguageServerId, resolve_state: ResolveState, force_no_type_left_padding: bool, - cx: &AppContext, - ) -> InlayHint { + cx: &mut AsyncAppContext, + ) -> anyhow::Result { let kind = lsp_hint.kind.and_then(|kind| match kind { lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), _ => None, }); - let buffer = buffer_handle.read(cx); - let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + + let position = cx.update(|cx| { + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + } + }); + let label = Self::lsp_inlay_label_to_project( + &buffer_handle, + project, + server_id, + lsp_hint.label, + cx, + ) + .await + .context("lsp to project inlay hint conversion")?; let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { false } else { lsp_hint.padding_left.unwrap_or(false) }; - InlayHint { - position: if kind == Some(InlayHintKind::Parameter) { - buffer.anchor_before(position) - } else { - buffer.anchor_after(position) - }, + + Ok(InlayHint { + position, padding_left, padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: match markup_content.kind { - lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, - }, - value: markup_content.value, - }) - } - }), - location: label_part.location.map(|lsp_location| { - let target_start = buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer_handle.clone(), - range: buffer.anchor_after(target_start) - ..buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ), - }, + label, kind, tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), @@ -1859,7 +1837,100 @@ impl InlayHints { } }), resolve_state, - } + }) + } + + async fn lsp_inlay_label_to_project( + buffer: &ModelHandle, + project: &ModelHandle, + server_id: LanguageServerId, + lsp_label: lsp::InlayHintLabel, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let label = match lsp_label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => { + let mut parts_data = Vec::with_capacity(lsp_parts.len()); + buffer.update(cx, |buffer, cx| { + for lsp_part in lsp_parts { + let location_buffer_task = match &lsp_part.location { + Some(lsp_location) => { + let location_buffer_task = project.update(cx, |project, cx| { + let language_server_name = project + .language_server_for_buffer(buffer, server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }); + Some(lsp_location.clone()).zip(location_buffer_task) + } + None => None, + }; + + parts_data.push((lsp_part, location_buffer_task)); + } + }); + + let mut parts = Vec::with_capacity(parts_data.len()); + for (lsp_part, location_buffer_task) in parts_data { + let location = match location_buffer_task { + Some((lsp_location, target_buffer_handle_task)) => { + let target_buffer_handle = target_buffer_handle_task + .await + .context("resolving location for label part buffer")?; + let range = cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }); + Some(Location { + buffer: target_buffer_handle, + range, + }) + } + None => None, + }; + + parts.push(InlayHintLabelPart { + value: lsp_part.value, + tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location, + }); + } + InlayHintLabel::LabelParts(parts) + } + }; + + Ok(label) } pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { @@ -2115,15 +2186,18 @@ impl InlayHints { }) }), location: part.location.and_then(|location| { - let path = cx.read(|cx| { - let project_path = location.buffer.read(cx).project_path(cx)?; - project.read(cx).absolute_path(&project_path, cx) + let (path, location_snapshot) = cx.read(|cx| { + let buffer = location.buffer.read(cx); + let project_path = buffer.project_path(cx)?; + let location_snapshot = buffer.snapshot(); + let path = project.read(cx).absolute_path(&project_path, cx); + path.zip(Some(location_snapshot)) })?; Some(lsp::Location::new( lsp::Url::from_file_path(path).unwrap(), range_to_lsp( - location.range.start.to_point_utf16(snapshot) - ..location.range.end.to_point_utf16(snapshot), + location.range.start.to_point_utf16(&location_snapshot) + ..location.range.end.to_point_utf16(&location_snapshot), ), )) }), @@ -2182,7 +2256,7 @@ impl LspCommand for InlayHints { buffer: ModelHandle, server_id: LanguageServerId, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning @@ -2194,32 +2268,38 @@ impl LspCommand for InlayHints { // Hence let's use a heuristic first to handle the most awkward case and look for more. let force_no_type_left_padding = lsp_adapter.name.0.as_ref() == "typescript-language-server"; - cx.read(|cx| { - Ok(message - .unwrap_or_default() - .into_iter() - .map(|lsp_hint| { - let resolve_state = match lsp_server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( - lsp::InlayHintOptions { - resolve_provider: Some(true), - .. - }, - ))) => { - ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) - } - _ => ResolveState::Resolved, - }; - InlayHints::lsp_to_project_hint( - lsp_hint, - &buffer, - resolve_state, - force_no_type_left_padding, - cx, - ) - }) - .collect()) - }) + + let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { + let resolve_state = match lsp_server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( + lsp::InlayHintOptions { + resolve_provider: Some(true), + .. + }, + ))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()), + _ => ResolveState::Resolved, + }; + + let project = project.clone(); + let buffer = buffer.clone(); + cx.spawn(|mut cx| async move { + InlayHints::lsp_to_project_hint( + lsp_hint, + &project, + &buffer, + server_id, + resolve_state, + force_no_type_left_padding, + &mut cx, + ) + .await + }) + }); + future::join_all(hints) + .await + .into_iter() + .collect::>() + .context("lsp to project inlay hints conversion") } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 65be7cceb10ef3a1fea39ba908d239b3948085bf..fbdbd04664fa4a76ebfa9e7992fd35987da8ad2f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5054,22 +5054,23 @@ impl Project { } let buffer_snapshot = buffer.snapshot(); - cx.spawn(|project, cx| async move { + cx.spawn(|project, mut cx| async move { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), ); let resolved_hint = resolve_task .await .context("inlay hint resolve LSP request")?; - let resolved_hint = cx.read(|cx| { - InlayHints::lsp_to_project_hint( - resolved_hint, - &buffer_handle, - ResolveState::Resolved, - false, - cx, - ) - }); + let resolved_hint = InlayHints::lsp_to_project_hint( + resolved_hint, + &project, + &buffer_handle, + server_id, + ResolveState::Resolved, + false, + &mut cx, + ) + .await?; Ok(Some(resolved_hint)) }) } else if let Some(project_id) = self.remote_id() { From 6c5761d05bccadfde383281560ad15e14f452c24 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 23:21:37 +0300 Subject: [PATCH 064/142] Pass inlay highlight information --- crates/editor/src/display_map.rs | 39 +++++- crates/editor/src/display_map/block_map.rs | 14 ++- crates/editor/src/display_map/fold_map.rs | 27 ++-- crates/editor/src/display_map/inlay_map.rs | 113 +++++++++-------- crates/editor/src/display_map/tab_map.rs | 36 ++++-- crates/editor/src/display_map/wrap_map.rs | 16 ++- crates/editor/src/editor.rs | 31 ++++- crates/editor/src/element.rs | 9 +- crates/editor/src/link_go_to_definition.rs | 139 +++++++++++++-------- 9 files changed, 279 insertions(+), 145 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9159253142e56c55ed1243e41a12ea79e7ca26ff..9df2919351d7563d272172bd6dd262dfd20af97b 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -4,7 +4,10 @@ mod inlay_map; mod tab_map; mod wrap_map; -use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use crate::{ + link_go_to_definition::InlayCoordinates, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + MultiBufferSnapshot, ToOffset, ToPoint, +}; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; use fold_map::FoldMap; @@ -40,6 +43,7 @@ pub trait ToDisplayPoint { } type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; +type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -50,6 +54,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -85,6 +90,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), + inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -109,6 +115,7 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), + inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -215,6 +222,16 @@ impl DisplayMap { .insert(Some(type_id), Arc::new((style, ranges))); } + pub fn highlight_inlays( + &mut self, + type_id: TypeId, + ranges: Vec, + style: HighlightStyle, + ) { + self.inlay_highlights + .insert(Some(type_id), Arc::new((style, ranges))); + } + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) @@ -227,6 +244,13 @@ impl DisplayMap { self.text_highlights.remove(&Some(type_id)) } + pub fn clear_inlay_highlights( + &mut self, + type_id: TypeId, + ) -> Option)>> { + self.inlay_highlights.remove(&Some(type_id)) + } + pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -296,6 +320,7 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -421,6 +446,7 @@ impl DisplaySnapshot { None, None, None, + None, ) .map(|h| h.text) } @@ -429,7 +455,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None) + .chunks(row..row + 1, false, None, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -441,15 +467,16 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - hint_highlights, - suggestion_highlights, + Some(&self.inlay_highlights), + inlay_highlight_style, + suggestion_highlight_style, ) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4b76ded3d50cc45d72385d70bbb424b139023f09..083d8be5f32e890b8593f5d090a94656f0813fb6 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,6 +1,6 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; @@ -579,6 +579,7 @@ impl BlockSnapshot { None, None, None, + None, ) .map(|chunk| chunk.text) .collect() @@ -589,8 +590,9 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -623,8 +625,9 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), transforms: cursor, @@ -1504,6 +1507,7 @@ mod tests { None, None, None, + None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 0b1523fe750326dea5c87f3ec4dcfa350f497185..800b51fcd850e767446c84408c99d53a70b2a0ab 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None, None) .map(|c| c.text) .collect() } @@ -652,8 +652,9 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,8 +676,9 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), inlay_chunk: None, inlay_offset: inlay_start, @@ -687,8 +689,15 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks(start.to_offset(self)..self.len(), false, None, None, None) - .flat_map(|chunk| chunk.text.chars()) + self.chunks( + start.to_offset(self)..self.len(), + false, + None, + None, + None, + None, + ) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1496,7 +1505,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, None, None, None, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 34181a576932d2aecc2cda538297fda365df4726..5617b6a6b67cf7de7bd95d2b29f351feeaba843c 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -15,7 +15,7 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::TextHighlights; +use super::{InlayHighlights, TextHighlights}; pub struct InlayMap { snapshot: InlaySnapshot, @@ -973,8 +973,9 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); @@ -983,53 +984,50 @@ impl InlaySnapshot { if let Some(text_highlights) = text_highlights { if !text_highlights.is_empty() { while cursor.start().0 < range.end { - if true { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); - - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; - - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - // TODO kb add a way to highlight inlay hints through here. - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); - highlight_endpoints.push(HighlightEndpoint { - offset: self - .to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; + + for (tag, highlights) in text_highlights.iter() { + let style = highlights.0; + let ranges = &highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less } + }) { + Ok(i) | Err(i) => i, + }; + // TODO kb add a way to highlight inlay hints through here. + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); } } @@ -1050,8 +1048,8 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: hint_highlights, - suggestion_highlight_style: suggestion_highlights, + hint_highlight_style: inlay_highlight_style, + suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), snapshot: self, @@ -1060,9 +1058,16 @@ impl InlaySnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(Default::default()..self.len(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + Default::default()..self.len(), + false, + None, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } fn check_invariants(&self) { @@ -1636,6 +1641,8 @@ mod tests { InlayOffset(start)..InlayOffset(end), false, Some(&highlights), + // TODO kb add tests + None, None, None, ) diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index ca73f6a1a7a7e5bff4d19a32db548c9d2155f744..187a8de1d30aa450cacaf35711b5586a95ab9fb5 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,6 +1,6 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; @@ -71,6 +71,7 @@ impl TabMap { None, None, None, + None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -183,7 +184,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None) + .chunks(range.start..line_end, false, None, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -203,6 +204,7 @@ impl TabSnapshot { None, None, None, + None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -223,9 +225,11 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, + // TODO kb extract into one struct text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); @@ -246,8 +250,9 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_column, column: expanded_char_column, @@ -270,9 +275,16 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + TabPoint::zero()..self.max_point(), + false, + None, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -600,6 +612,7 @@ mod tests { None, None, None, + None, ) .map(|c| c.text) .collect::(), @@ -674,7 +687,8 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None, None) + { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -743,7 +757,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, None, None, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f21c7151ad695b2567dc9cea7da5a5007be1696f..f8287af799890e59b1b4b1c72f845fd1d26a76d5 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,7 +1,7 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::{ @@ -447,6 +447,7 @@ impl WrapSnapshot { None, None, None, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -576,8 +577,9 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -595,8 +597,9 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), output_position: output_start, @@ -1326,6 +1329,7 @@ mod tests { None, None, None, + None, ) .map(|h| h.text) } @@ -1350,7 +1354,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None) + .chunks(start_row..end_row, true, None, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fb1e87580aa8bed7b994e8c8bc92659ffd096d71..f10734a76c24c675016ca4c448ff4ebfe5df15b2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,7 +64,9 @@ use language::{ Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::{hide_link_definition, show_link_definition, LinkGoToDefinitionState}; +use link_go_to_definition::{ + hide_link_definition, show_link_definition, InlayCoordinates, LinkGoToDefinitionState, +}; use log::error; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -7716,6 +7718,18 @@ impl Editor { cx.notify(); } + pub fn highlight_inlays( + &mut self, + ranges: Vec, + style: HighlightStyle, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, @@ -7736,6 +7750,19 @@ impl Editor { highlights } + pub fn clear_highlights( + &mut self, + cx: &mut ViewContext, + ) -> Option>)>> { + let highlights = self + .display_map + .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); + if highlights.is_some() { + cx.notify(); + } + highlights + } + pub fn show_local_cursors(&self, cx: &AppContext) -> bool { self.blink_manager.read(cx).visible() && self.focused } @@ -8327,7 +8354,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_text_highlights::(cx); + self.clear_highlights::(cx); } false diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c28f14d98a916ab93a57be52fb20f69a487ee2ad..c448ba4f22bea1e90506044fbc97d457136f320d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,7 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, + GoToDefinitionTrigger, InlayCoordinates, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -1927,7 +1927,12 @@ fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, GoToDefinitionTrigger::InlayHint( - hovered_hint.position, + InlayCoordinates { + inlay_id: hovered_hint.id, + inlay_position: hovered_hint.position, + inlay_start: hint_start_offset, + highlight_end: hovered_offset, + }, LocationLink { origin: Some(Location { buffer, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index b043af613279e44068d483c25ec17a2f752354dd..35e367497e65da8fb0e581f209e1c3e64537a7b9 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,4 +1,7 @@ -use crate::{element::PointForPosition, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{ + display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, + EditorSnapshot, InlayId, SelectPhase, +}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; @@ -7,8 +10,8 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_trigger_point: Option, - pub symbol_range: Option>, + pub last_trigger_point: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, @@ -16,34 +19,65 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(Anchor, LocationLink), + InlayHint(InlayCoordinates, LocationLink), None, } +#[derive(Debug, Clone, Copy)] +pub struct InlayCoordinates { + pub inlay_id: InlayId, + pub inlay_position: Anchor, + pub inlay_start: InlayOffset, + pub highlight_end: InlayOffset, +} + #[derive(Debug, Clone)] -pub enum TriggerAnchor { +pub enum TriggerPoint { Text(Anchor), - InlayHint(Anchor, LocationLink), + InlayHint(InlayCoordinates, LocationLink), +} + +#[derive(Debug, Clone)] +pub enum SymbolRange { + Text(Range), + Inlay(InlayCoordinates), } -impl TriggerAnchor { +impl SymbolRange { + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { + match (self, trigger_point) { + (SymbolRange::Text(range), TriggerPoint::Text(point)) => { + let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); + point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() + } + (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + range.inlay_start.cmp(&point.highlight_end).is_le() + && range.highlight_end.cmp(&point.highlight_end).is_ge() + } + (SymbolRange::Inlay(_), TriggerPoint::Text(_)) + | (SymbolRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + } + } +} + +impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { - TriggerAnchor::Text(anchor) => anchor, - TriggerAnchor::InlayHint(anchor, _) => anchor, + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(coordinates, _) => &coordinates.inlay_position, } } pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { - TriggerAnchor::Text(_) => { + TriggerPoint::Text(_) => { if shift { LinkDefinitionKind::Type } else { LinkDefinitionKind::Symbol } } - TriggerAnchor::InlayHint(_, _) => LinkDefinitionKind::Type, + TriggerPoint::InlayHint(_, _) => LinkDefinitionKind::Type, } } } @@ -61,11 +95,11 @@ pub fn update_go_to_definition_link( let snapshot = editor.snapshot(cx); let trigger_point = match origin { GoToDefinitionTrigger::Text(p) => { - Some(TriggerAnchor::Text(snapshot.buffer_snapshot.anchor_before( + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerAnchor::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), GoToDefinitionTrigger::None => None, }; @@ -109,7 +143,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: TriggerAnchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -122,7 +156,7 @@ pub fn show_link_definition( return; } - let trigger_anchor = trigger_point.anchor().clone(); + let trigger_anchor = trigger_point.anchor(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) @@ -151,26 +185,15 @@ pub fn show_link_definition( // Don't request again if the location is within the symbol region of a previous request with the same kind if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { - let point_after_start = symbol_range - .start - .cmp(&trigger_anchor, &snapshot.buffer_snapshot) - .is_le(); - - let point_before_end = symbol_range - .end - .cmp(&trigger_anchor, &snapshot.buffer_snapshot) - .is_ge(); - - let point_within_range = point_after_start && point_before_end; - if point_within_range && same_kind { + if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) { return; } } let task = cx.spawn(|this, mut cx| { async move { - let result = match trigger_point { - TriggerAnchor::Text(_) => { + let result = match &trigger_point { + TriggerPoint::Text(_) => { // query the LSP for definition info cx.update(|cx| { project.update(cx, |project, cx| match definition_kind { @@ -196,23 +219,22 @@ pub fn show_link_definition( .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - start..end + SymbolRange::Text(start..end) }) }), definition_result, ) }) } - TriggerAnchor::InlayHint(trigger_source, trigger_target) => { - // TODO kb range is wrong, should be in inlay coordinates have a proper inlay range. - // Or highlight inlays differently, in their layer? - Some((Some(trigger_source..trigger_source), vec![trigger_target])) - } + TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( + Some(SymbolRange::Inlay(trigger_source.clone())), + vec![trigger_target.clone()], + )), }; this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_text_highlights::(cx); + this.clear_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -248,22 +270,37 @@ pub fn show_link_definition( }); if any_definition_does_not_contain_current_location { - // If no symbol range returned from language server, use the surrounding word. - let highlight_range = symbol_range.unwrap_or_else(|| { - let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end) - }); - // Highlight symbol using theme link definition highlight style let style = theme::current(cx).editor.link_definition; - this.highlight_text::( - vec![highlight_range], - style, - cx, - ); + let highlight_range = symbol_range.unwrap_or_else(|| match trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); + SymbolRange::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(inlay_trigger, _) => { + SymbolRange::Inlay(inlay_trigger) + } + }); + + match highlight_range { + SymbolRange::Text(text_range) => this + .highlight_text::( + vec![text_range], + style, + cx, + ), + SymbolRange::Inlay(inlay_coordinates) => this + .highlight_inlays::( + vec![inlay_coordinates], + style, + cx, + ), + } } else { hide_link_definition(this, cx); } @@ -289,7 +326,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_text_highlights::(cx); + editor.clear_highlights::(cx); } pub fn go_to_fetched_definition( From f8874a726cc4a3fcc09613d8faa7a215ad8dc915 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 01:02:25 +0300 Subject: [PATCH 065/142] Attempt to highlight inlays --- crates/editor/src/display_map/inlay_map.rs | 168 +++++++++++++++------ crates/editor/src/editor.rs | 13 +- crates/editor/src/element.rs | 23 +-- crates/editor/src/link_go_to_definition.rs | 11 +- 4 files changed, 143 insertions(+), 72 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 5617b6a6b67cf7de7bd95d2b29f351feeaba843c..23b39ca5ab0f021853ffe05017e3ec223076830f 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -2,7 +2,7 @@ use crate::{ multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet}; +use collections::{BTreeMap, BTreeSet, HashSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use std::{ @@ -183,7 +183,7 @@ pub struct InlayBufferRows<'a> { max_buffer_row: u32, } -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] struct HighlightEndpoint { offset: InlayOffset, is_start: bool, @@ -243,6 +243,7 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } + // TODO kb highlights are not displayed still let mut next_highlight_endpoint = InlayOffset(usize::MAX); while let Some(endpoint) = self.highlight_endpoints.peek().copied() { if endpoint.offset <= self.output_offset { @@ -980,62 +981,89 @@ impl InlaySnapshot { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); + let empty_text_highlights = TextHighlights::default(); + let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); + let empty_inlay_highlights = InlayHighlights::default(); + let inlay_highlights = inlay_highlights.unwrap_or_else(|| &empty_inlay_highlights); + let mut highlight_endpoints = Vec::new(); - if let Some(text_highlights) = text_highlights { - if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); + if !text_highlights.is_empty() || !inlay_highlights.is_empty() { + while cursor.start().0 < range.end { + let transform_start = self + .buffer + .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); + + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; + let mut covered_tags = HashSet::default(); + for (tag, text_highlights) in text_highlights.iter() { + covered_tags.insert(*tag); + let style = text_highlights.0; + let ranges = &text_highlights.1; - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - // TODO kb add a way to highlight inlay hints through here. - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); + } - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); - } + if let Some(inlay_highlights) = inlay_highlights.get(tag) { + self.push_inlay_highlight_range( + inlay_highlights, + transform_start, + transform_end, + &mut highlight_endpoints, + tag, + ); } + } - cursor.next(&()); + for (tag, inlay_highlights) in inlay_highlights + .iter() + .filter(|(tag, _)| !covered_tags.contains(tag)) + { + self.push_inlay_highlight_range( + inlay_highlights, + transform_start, + transform_end, + &mut highlight_endpoints, + tag, + ); } - highlight_endpoints.sort(); - cursor.seek(&range.start, Bias::Right, &()); + + cursor.next(&()); } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); } let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1056,6 +1084,48 @@ impl InlaySnapshot { } } + fn push_inlay_highlight_range( + &self, + inlay_highlights: &std::sync::Arc<( + HighlightStyle, + Vec, + )>, + transform_start: Anchor, + transform_end: Anchor, + highlight_endpoints: &mut Vec, + tag: &Option, + ) { + let style = inlay_highlights.0; + let ranges = &inlay_highlights.1; + let start_ix = match ranges + .binary_search_by(|probe| probe.inlay_position.cmp(&transform_start, &self.buffer)) + { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .inlay_position + .cmp(&transform_end, &self.buffer) + .is_ge() + { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.highlight_start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.highlight_end, + is_start: false, + tag: *tag, + style, + }); + } + } + #[cfg(test)] pub fn text(&self) -> String { self.chunks( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f10734a76c24c675016ca4c448ff4ebfe5df15b2..6c5d1a45ac05b65f4a214e5a7453c3a642d776f3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7750,17 +7750,16 @@ impl Editor { highlights } - pub fn clear_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self + pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { + let inlay_highlights = self + .display_map + .update(cx, |map, _| map.clear_inlay_highlights(TypeId::of::())); + if text_highlights.is_some() || inlay_highlights.is_some() { cx.notify(); } - highlights } pub fn show_local_cursors(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c448ba4f22bea1e90506044fbc97d457136f320d..10809d896296bf5e85fb04ccdee5a564b7cfab25 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1908,11 +1908,13 @@ fn update_inlay_link_and_hover_points( } } project::InlayHintLabel::LabelParts(label_parts) => { - if let Some(hovered_hint_part) = find_hovered_hint_part( - label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) { + if let Some((hovered_hint_part, part_range)) = + find_hovered_hint_part( + label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) + { if hovered_hint_part.tooltip.is_some() { dbg!(&hovered_hint_part.tooltip); // TODO kb // hover_at_point = Some(hovered_offset); @@ -1928,10 +1930,9 @@ fn update_inlay_link_and_hover_points( editor, GoToDefinitionTrigger::InlayHint( InlayCoordinates { - inlay_id: hovered_hint.id, inlay_position: hovered_hint.position, - inlay_start: hint_start_offset, - highlight_end: hovered_offset, + highlight_start: part_range.start, + highlight_end: part_range.end, }, LocationLink { origin: Some(Location { @@ -1976,15 +1977,17 @@ fn find_hovered_hint_part( label_parts: Vec, hint_range: Range, hovered_offset: InlayOffset, -) -> Option { +) -> Option<(InlayHintLabelPart, Range)> { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; + part_start.0 += part_len; } else { - return Some(part); + return Some((part, part_start..InlayOffset(part_start.0 + part_len))); } } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 35e367497e65da8fb0e581f209e1c3e64537a7b9..ee2b536042662055e2e252990c9689caf59be705 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,6 +1,6 @@ use crate::{ display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, - EditorSnapshot, InlayId, SelectPhase, + EditorSnapshot, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; @@ -25,9 +25,8 @@ pub enum GoToDefinitionTrigger { #[derive(Debug, Clone, Copy)] pub struct InlayCoordinates { - pub inlay_id: InlayId, pub inlay_position: Anchor, - pub inlay_start: InlayOffset, + pub highlight_start: InlayOffset, pub highlight_end: InlayOffset, } @@ -51,7 +50,7 @@ impl SymbolRange { point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { - range.inlay_start.cmp(&point.highlight_end).is_le() + range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } (SymbolRange::Inlay(_), TriggerPoint::Text(_)) @@ -282,8 +281,8 @@ pub fn show_link_definition( ..snapshot.anchor_after(offset_range.end), ) } - TriggerPoint::InlayHint(inlay_trigger, _) => { - SymbolRange::Inlay(inlay_trigger) + TriggerPoint::InlayHint(inlay_coordinates, _) => { + SymbolRange::Inlay(inlay_coordinates) } }); From 4cc9f2f525e9e2991f1c71cf5a2582bfa492ec83 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 11:34:40 +0300 Subject: [PATCH 066/142] Highlight inlay hint parts on cmd-hover Co-Authored-By: Antonio --- crates/editor/src/display_map/inlay_map.rs | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 23b39ca5ab0f021853ffe05017e3ec223076830f..19b3ec792425117f09b3dcc259642af15dcae743 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -210,6 +210,7 @@ pub struct InlayChunks<'a> { buffer_chunks: MultiBufferChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, + inlay_chunk: Option<&'a str>, output_offset: InlayOffset, max_output_offset: InlayOffset, hint_highlight_style: Option, @@ -298,13 +299,31 @@ impl<'a> Iterator for InlayChunks<'a> { - self.transforms.start().0; inlay.text.chunks_in_range(start.0..end.0) }); + let inlay_chunk = self + .inlay_chunk + .get_or_insert_with(|| inlay_chunks.next().unwrap()); + let (chunk, remainder) = inlay_chunk.split_at( + inlay_chunk + .len() + .min(next_highlight_endpoint.0 - self.output_offset.0), + ); + *inlay_chunk = remainder; + if inlay_chunk.is_empty() { + self.inlay_chunk = None; + } - let chunk = inlay_chunks.next().unwrap(); self.output_offset.0 += chunk.len(); - let highlight_style = match inlay.id { + let mut highlight_style = match inlay.id { InlayId::Suggestion(_) => self.suggestion_highlight_style, InlayId::Hint(_) => self.hint_highlight_style, }; + if !self.active_highlights.is_empty() { + for active_highlight in self.active_highlights.values() { + highlight_style + .get_or_insert(Default::default()) + .highlight(*active_highlight); + } + } Chunk { text: chunk, highlight_style, @@ -1073,6 +1092,7 @@ impl InlaySnapshot { transforms: cursor, buffer_chunks, inlay_chunks: None, + inlay_chunk: None, buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, From 420f8b7b1597e7ea132cba5ffd4f5459834aef9d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 22:06:48 +0300 Subject: [PATCH 067/142] Prepare for inlay and text highlight unification --- crates/editor/src/display_map.rs | 8 ++--- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/editor.rs | 21 +++--------- crates/editor/src/element.rs | 4 +-- crates/editor/src/link_go_to_definition.rs | 38 +++++++++++----------- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9df2919351d7563d272172bd6dd262dfd20af97b..4f08be73b9400ce1c3bbdf59979a19cb90af1e22 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,7 +5,7 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayCoordinates, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + link_go_to_definition::InlayRange, Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; @@ -43,7 +43,7 @@ pub trait ToDisplayPoint { } type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -225,7 +225,7 @@ impl DisplayMap { pub fn highlight_inlays( &mut self, type_id: TypeId, - ranges: Vec, + ranges: Vec, style: HighlightStyle, ) { self.inlay_highlights @@ -247,7 +247,7 @@ impl DisplayMap { pub fn clear_inlay_highlights( &mut self, type_id: TypeId, - ) -> Option)>> { + ) -> Option)>> { self.inlay_highlights.remove(&Some(type_id)) } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 19b3ec792425117f09b3dcc259642af15dcae743..cc730f933321cdc89b3b76d7ca5a436261b6d8a4 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1108,7 +1108,7 @@ impl InlaySnapshot { &self, inlay_highlights: &std::sync::Arc<( HighlightStyle, - Vec, + Vec, )>, transform_start: Anchor, transform_end: Anchor, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6c5d1a45ac05b65f4a214e5a7453c3a642d776f3..af352d2649e387a47125da14ae1ac335a8cb6800 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use language::{ OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, InlayCoordinates, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, InlayRange, LinkGoToDefinitionState, }; use log::error; use multi_buffer::ToOffsetUtf16; @@ -7720,7 +7720,7 @@ impl Editor { pub fn highlight_inlays( &mut self, - ranges: Vec, + ranges: Vec, style: HighlightStyle, cx: &mut ViewContext, ) { @@ -7737,20 +7737,7 @@ impl Editor { self.display_map.read(cx).text_highlights(TypeId::of::()) } - pub fn clear_text_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self - .display_map - .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { - cx.notify(); - } - highlights - } - - pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + pub fn clear_text_highlights(&mut self, cx: &mut ViewContext) { let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); @@ -8353,7 +8340,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_highlights::(cx); + self.clear_text_highlights::(cx); } false diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 10809d896296bf5e85fb04ccdee5a564b7cfab25..2be5105b33493724e43d3a65b4a631b3551f2139 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,7 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, InlayCoordinates, + GoToDefinitionTrigger, InlayRange, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -1929,7 +1929,7 @@ fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, GoToDefinitionTrigger::InlayHint( - InlayCoordinates { + InlayRange { inlay_position: hovered_hint.position, highlight_start: part_range.start, highlight_end: part_range.end, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ee2b536042662055e2e252990c9689caf59be705..5bc45720a2be411c88d312a2a368c6e0f08bd9c4 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -11,7 +11,7 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { pub last_trigger_point: Option, - pub symbol_range: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, @@ -19,12 +19,12 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(InlayCoordinates, LocationLink), + InlayHint(InlayRange, LocationLink), None, } #[derive(Debug, Clone, Copy)] -pub struct InlayCoordinates { +pub struct InlayRange { pub inlay_position: Anchor, pub highlight_start: InlayOffset, pub highlight_end: InlayOffset, @@ -33,28 +33,28 @@ pub struct InlayCoordinates { #[derive(Debug, Clone)] pub enum TriggerPoint { Text(Anchor), - InlayHint(InlayCoordinates, LocationLink), + InlayHint(InlayRange, LocationLink), } #[derive(Debug, Clone)] -pub enum SymbolRange { +pub enum DocumentRange { Text(Range), - Inlay(InlayCoordinates), + Inlay(InlayRange), } -impl SymbolRange { +impl DocumentRange { fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { - (SymbolRange::Text(range), TriggerPoint::Text(point)) => { + (DocumentRange::Text(range), TriggerPoint::Text(point)) => { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } - (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } - (SymbolRange::Inlay(_), TriggerPoint::Text(_)) - | (SymbolRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + (DocumentRange::Inlay(_), TriggerPoint::Text(_)) + | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, } } } @@ -218,7 +218,7 @@ pub fn show_link_definition( .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - SymbolRange::Text(start..end) + DocumentRange::Text(start..end) }) }), definition_result, @@ -226,14 +226,14 @@ pub fn show_link_definition( }) } TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( - Some(SymbolRange::Inlay(trigger_source.clone())), + Some(DocumentRange::Inlay(trigger_source.clone())), vec![trigger_target.clone()], )), }; this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_highlights::(cx); + this.clear_text_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -276,24 +276,24 @@ pub fn show_link_definition( let snapshot = &snapshot.buffer_snapshot; // If no symbol range returned from language server, use the surrounding word. let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - SymbolRange::Text( + DocumentRange::Text( snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end), ) } TriggerPoint::InlayHint(inlay_coordinates, _) => { - SymbolRange::Inlay(inlay_coordinates) + DocumentRange::Inlay(inlay_coordinates) } }); match highlight_range { - SymbolRange::Text(text_range) => this + DocumentRange::Text(text_range) => this .highlight_text::( vec![text_range], style, cx, ), - SymbolRange::Inlay(inlay_coordinates) => this + DocumentRange::Inlay(inlay_coordinates) => this .highlight_inlays::( vec![inlay_coordinates], style, @@ -325,7 +325,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_highlights::(cx); + editor.clear_text_highlights::(cx); } pub fn go_to_fetched_definition( From 12ffbe54fb2c07ceafd2722aea9ee767a31e8c72 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 22:38:49 +0300 Subject: [PATCH 068/142] Unify text and inlay highlights --- crates/editor/src/display_map.rs | 43 +++---- crates/editor/src/display_map/block_map.rs | 6 +- crates/editor/src/display_map/fold_map.rs | 19 +-- crates/editor/src/display_map/inlay_map.rs | 113 +++++------------- crates/editor/src/display_map/tab_map.rs | 29 ++--- crates/editor/src/display_map/wrap_map.rs | 8 +- crates/editor/src/editor.rs | 13 +- crates/editor/src/link_go_to_definition.rs | 7 ++ crates/editor/src/test/editor_test_context.rs | 1 + 9 files changed, 76 insertions(+), 163 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 4f08be73b9400ce1c3bbdf59979a19cb90af1e22..037854435b6ee588044c57a96b08c5b0b933fb67 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,8 +5,8 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayRange, Anchor, AnchorRangeExt, InlayId, MultiBuffer, - MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; @@ -42,8 +42,7 @@ pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } -type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -54,7 +53,6 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -90,7 +88,6 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), - inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -115,7 +112,6 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), - inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -218,8 +214,10 @@ impl DisplayMap { ranges: Vec>, style: HighlightStyle, ) { - self.text_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())), + ); } pub fn highlight_inlays( @@ -228,11 +226,16 @@ impl DisplayMap { ranges: Vec, style: HighlightStyle, ) { - self.inlay_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new(( + style, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + )), + ); } - pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) } @@ -240,17 +243,10 @@ impl DisplayMap { pub fn clear_text_highlights( &mut self, type_id: TypeId, - ) -> Option>)>> { + ) -> Option)>> { self.text_highlights.remove(&Some(type_id)) } - pub fn clear_inlay_highlights( - &mut self, - type_id: TypeId, - ) -> Option)>> { - self.inlay_highlights.remove(&Some(type_id)) - } - pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -320,7 +316,6 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -446,7 +441,6 @@ impl DisplaySnapshot { None, None, None, - None, ) .map(|h| h.text) } @@ -455,7 +449,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None, None) + .chunks(row..row + 1, false, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -474,7 +468,6 @@ impl DisplaySnapshot { display_rows, language_aware, Some(&self.text_highlights), - Some(&self.inlay_highlights), inlay_highlight_style, suggestion_highlight_style, ) @@ -797,7 +790,7 @@ impl DisplaySnapshot { #[cfg(any(test, feature = "test-support"))] pub fn highlight_ranges( &self, - ) -> Option>)>> { + ) -> Option)>> { let type_id = TypeId::of::(); self.text_highlights.get(&Some(type_id)).cloned() } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 083d8be5f32e890b8593f5d090a94656f0813fb6..8577e928199a1af11612a813bd1c0648a2f409d4 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,6 +1,6 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; @@ -579,7 +579,6 @@ impl BlockSnapshot { None, None, None, - None, ) .map(|chunk| chunk.text) .collect() @@ -590,7 +589,6 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> BlockChunks<'a> { @@ -625,7 +623,6 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -1507,7 +1504,6 @@ mod tests { None, None, None, - None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 800b51fcd850e767446c84408c99d53a70b2a0ab..dcbc156c47f1dbc65d10c013095996898d4ca32b 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None) .map(|c| c.text) .collect() } @@ -652,7 +652,6 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> FoldChunks<'a> { @@ -676,7 +675,6 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -689,15 +687,8 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks( - start.to_offset(self)..self.len(), - false, - None, - None, - None, - None, - ) - .flat_map(|chunk| chunk.text.chars()) + self.chunks(start.to_offset(self)..self.len(), false, None, None, None) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1505,7 +1496,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index cc730f933321cdc89b3b76d7ca5a436261b6d8a4..06d166b86cab6151cd4a92837813238c9457dcda 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,8 +1,9 @@ use crate::{ + link_go_to_definition::DocumentRange, multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet, HashSet}; +use collections::{BTreeMap, BTreeSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use std::{ @@ -15,7 +16,7 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::{InlayHighlights, TextHighlights}; +use super::TextHighlights; pub struct InlayMap { snapshot: InlaySnapshot, @@ -244,7 +245,6 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } - // TODO kb highlights are not displayed still let mut next_highlight_endpoint = InlayOffset(usize::MAX); while let Some(endpoint) = self.highlight_endpoints.peek().copied() { if endpoint.offset <= self.output_offset { @@ -993,7 +993,6 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> InlayChunks<'a> { @@ -1002,15 +1001,14 @@ impl InlaySnapshot { let empty_text_highlights = TextHighlights::default(); let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); - let empty_inlay_highlights = InlayHighlights::default(); - let inlay_highlights = inlay_highlights.unwrap_or_else(|| &empty_inlay_highlights); let mut highlight_endpoints = Vec::new(); - if !text_highlights.is_empty() || !inlay_highlights.is_empty() { + if !text_highlights.is_empty() { while cursor.start().0 < range.end { let transform_start = self .buffer .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); + let transform_start = self.to_inlay_offset(transform_start.to_offset(&self.buffer)); let transform_end = { let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); @@ -1019,15 +1017,17 @@ impl InlaySnapshot { cursor.start().0 + overshoot, ))) }; + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - let mut covered_tags = HashSet::default(); for (tag, text_highlights) in text_highlights.iter() { - covered_tags.insert(*tag); let style = text_highlights.0; let ranges = &text_highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); if cmp.is_gt() { cmp::Ordering::Greater } else { @@ -1037,46 +1037,24 @@ impl InlaySnapshot { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { break; } highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + offset: range.start, is_start: true, tag: *tag, style, }); highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + offset: range.end, is_start: false, tag: *tag, style, }); } - - if let Some(inlay_highlights) = inlay_highlights.get(tag) { - self.push_inlay_highlight_range( - inlay_highlights, - transform_start, - transform_end, - &mut highlight_endpoints, - tag, - ); - } - } - - for (tag, inlay_highlights) in inlay_highlights - .iter() - .filter(|(tag, _)| !covered_tags.contains(tag)) - { - self.push_inlay_highlight_range( - inlay_highlights, - transform_start, - transform_end, - &mut highlight_endpoints, - tag, - ); } cursor.next(&()); @@ -1104,60 +1082,23 @@ impl InlaySnapshot { } } - fn push_inlay_highlight_range( - &self, - inlay_highlights: &std::sync::Arc<( - HighlightStyle, - Vec, - )>, - transform_start: Anchor, - transform_end: Anchor, - highlight_endpoints: &mut Vec, - tag: &Option, - ) { - let style = inlay_highlights.0; - let ranges = &inlay_highlights.1; - let start_ix = match ranges - .binary_search_by(|probe| probe.inlay_position.cmp(&transform_start, &self.buffer)) - { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .inlay_position - .cmp(&transform_end, &self.buffer) - .is_ge() - { - break; + fn document_to_inlay_range(&self, range: &DocumentRange) -> Range { + match range { + DocumentRange::Text(text_range) => { + self.to_inlay_offset(text_range.start.to_offset(&self.buffer)) + ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer)) + } + DocumentRange::Inlay(inlay_range) => { + inlay_range.highlight_start..inlay_range.highlight_end } - - highlight_endpoints.push(HighlightEndpoint { - offset: range.highlight_start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.highlight_end, - is_start: false, - tag: *tag, - style, - }); } } #[cfg(test)] pub fn text(&self) -> String { - self.chunks( - Default::default()..self.len(), - false, - None, - None, - None, - None, - ) - .map(|chunk| chunk.text) - .collect() + self.chunks(Default::default()..self.len(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() } fn check_invariants(&self) { @@ -1651,6 +1592,8 @@ mod tests { .map(|range| { buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) }) + // TODO add inlay highlight tests + .map(DocumentRange::Text) .collect::>(); highlights.insert( @@ -1731,8 +1674,6 @@ mod tests { InlayOffset(start)..InlayOffset(end), false, Some(&highlights), - // TODO kb add tests - None, None, None, ) diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 187a8de1d30aa450cacaf35711b5586a95ab9fb5..cae9ccc91f4b0a9bb2c9d208f96b82157ce5a176 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,6 +1,6 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; @@ -71,7 +71,6 @@ impl TabMap { None, None, None, - None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -184,7 +183,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None, None) + .chunks(range.start..line_end, false, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -204,7 +203,6 @@ impl TabSnapshot { None, None, None, - None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -225,9 +223,8 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - // TODO kb extract into one struct + // TODO kb extract into one struct? text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> TabChunks<'a> { @@ -250,7 +247,6 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -275,16 +271,9 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks( - TabPoint::zero()..self.max_point(), - false, - None, - None, - None, - None, - ) - .map(|chunk| chunk.text) - .collect() + self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -612,7 +601,6 @@ mod tests { None, None, None, - None, ) .map(|c| c.text) .collect::(), @@ -687,8 +675,7 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None, None) - { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -757,7 +744,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f8287af799890e59b1b4b1c72f845fd1d26a76d5..d4b52d5893328e5e5abf50c20c842cf8c4a5622b 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,7 +1,7 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::{ @@ -447,7 +447,6 @@ impl WrapSnapshot { None, None, None, - None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -577,7 +576,6 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> WrapChunks<'a> { @@ -597,7 +595,6 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -1329,7 +1326,6 @@ mod tests { None, None, None, - None, ) .map(|h| h.text) } @@ -1354,7 +1350,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None, None) + .chunks(start_row..end_row, true, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index af352d2649e387a47125da14ae1ac335a8cb6800..99db78f1c2635950452ae6fafaf46be1cc41db8c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use language::{ OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, InlayRange, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, DocumentRange, InlayRange, LinkGoToDefinitionState, }; use log::error; use multi_buffer::ToOffsetUtf16; @@ -7733,7 +7733,7 @@ impl Editor { pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, - ) -> Option<(HighlightStyle, &'a [Range])> { + ) -> Option<(HighlightStyle, &'a [DocumentRange])> { self.display_map.read(cx).text_highlights(TypeId::of::()) } @@ -7741,10 +7741,7 @@ impl Editor { let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - let inlay_highlights = self - .display_map - .update(cx, |map, _| map.clear_inlay_highlights(TypeId::of::())); - if text_highlights.is_some() || inlay_highlights.is_some() { + if text_highlights.is_some() { cx.notify(); } } @@ -7953,6 +7950,8 @@ impl Editor { Some( ranges .iter() + // TODO kb mark inlays too + .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) }) @@ -8406,6 +8405,8 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; + // TODO kb mark inlays too + let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 5bc45720a2be411c88d312a2a368c6e0f08bd9c4..30f273065a54623edc084534e60e18869db00187 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -43,6 +43,13 @@ pub enum DocumentRange { } impl DocumentRange { + pub fn as_text_range(&self) -> Option> { + match self { + Self::Text(range) => Some(range.clone()), + Self::Inlay(_) => None, + } + } + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { (DocumentRange::Text(range), TriggerPoint::Text(point)) => { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 118cddaa9226a543ca479f577428237d77539d5d..8d7b6aa5c98700741dd5d17370bd9e3f97024a86 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -240,6 +240,7 @@ impl<'a> EditorTestContext<'a> { .map(|ranges| ranges.as_ref().clone().1) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect(); assert_set_eq!(actual_ranges, expected_ranges); From 4b78678923018f47dc80747dd9e826564edd9a21 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 13:24:17 +0300 Subject: [PATCH 069/142] Prepare background highlights for inlay highlights --- crates/editor/src/display_map.rs | 14 +- crates/editor/src/display_map/inlay_map.rs | 4 + crates/editor/src/editor.rs | 163 +++++++++--------- crates/editor/src/test/editor_test_context.rs | 1 + 4 files changed, 96 insertions(+), 86 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 037854435b6ee588044c57a96b08c5b0b933fb67..34e877a4483e70169b92d8a075d38bc116c0cef2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -416,8 +416,18 @@ impl DisplaySnapshot { .to_offset(self.display_point_to_inlay_point(point, bias)) } - pub fn inlay_point_to_inlay_offset(&self, point: InlayPoint) -> InlayOffset { - self.inlay_snapshot.to_offset(point) + pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset { + self.inlay_snapshot + .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot)) + } + + pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint { + let inlay_point = self.inlay_snapshot.to_point(offset); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) } fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 06d166b86cab6151cd4a92837813238c9457dcda..5094d2fab9595b613f183f587de163d1b039df51 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -392,6 +392,10 @@ impl InlayPoint { pub fn row(self) -> u32 { self.0.row } + + pub fn column(self) -> u32 { + self.0.column + } } impl InlayMap { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 99db78f1c2635950452ae6fafaf46be1cc41db8c..2089fe0f5fa9ce839df6519f27bbea6ac51d21fa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -535,6 +535,8 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; +type BackgroundHighlight = (fn(&Theme) -> Color, Vec); + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -564,8 +566,7 @@ pub struct Editor { show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, - #[allow(clippy::type_complexity)] - background_highlights: BTreeMap Color, Vec>)>, + background_highlights: BTreeMap, nav_history: Option, context_menu: Option, mouse_context_menu: ViewHandle, @@ -6758,10 +6759,18 @@ impl Editor { let rename_range = if let Some(range) = prepare_rename.await? { Some(range) } else { - this.read_with(&cx, |this, cx| { + this.update(&mut cx, |this, cx| { let buffer = this.buffer.read(cx).snapshot(cx); + let display_snapshot = this + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); let mut buffer_highlights = this - .document_highlights_for_position(selection.head(), &buffer) + .document_highlights_for_position( + selection.head(), + &buffer, + &display_snapshot, + ) + .filter_map(|highlight| highlight.as_text_range()) .filter(|highlight| { highlight.start.excerpt_id() == selection.head().excerpt_id() && highlight.end.excerpt_id() == selection.head().excerpt_id() @@ -6816,11 +6825,15 @@ impl Editor { let ranges = this .clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges) + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }) .chain( this.clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges), + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }), ) .collect(); @@ -7488,16 +7501,20 @@ impl Editor { color_fetcher: fn(&Theme) -> Color, cx: &mut ViewContext, ) { - self.background_highlights - .insert(TypeId::of::(), (color_fetcher, ranges)); + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Text).collect(), + ), + ); cx.notify(); } - #[allow(clippy::type_complexity)] pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, - ) -> Option<(fn(&Theme) -> Color, Vec>)> { + ) -> Option { let highlights = self.background_highlights.remove(&TypeId::of::()); if highlights.is_some() { cx.notify(); @@ -7522,7 +7539,8 @@ impl Editor { &'a self, position: Anchor, buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator> { + display_snapshot: &'a DisplaySnapshot, + ) -> impl 'a + Iterator { let read_highlights = self .background_highlights .get(&TypeId::of::()) @@ -7531,14 +7549,16 @@ impl Editor { .background_highlights .get(&TypeId::of::()) .map(|h| &h.1); - let left_position = position.bias_left(buffer); - let right_position = position.bias_right(buffer); + let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer)); + let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer)); read_highlights .into_iter() .chain(write_highlights) .flat_map(move |ranges| { let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&left_position, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&left_position); if cmp.is_ge() { Ordering::Greater } else { @@ -7549,9 +7569,12 @@ impl Editor { }; let right_position = right_position.clone(); - ranges[start_ix..] - .iter() - .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) + ranges[start_ix..].iter().take_while(move |range| { + document_to_inlay_range(range, &display_snapshot) + .start + .cmp(&right_position) + .is_le() + }) }) } @@ -7561,12 +7584,15 @@ impl Editor { display_snapshot: &DisplaySnapshot, theme: &Theme, ) -> Vec<(Range, Color)> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; for (color_fetcher, ranges) in self.background_highlights.values() { let color = color_fetcher(theme); let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7576,61 +7602,16 @@ impl Editor { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, color)) - } - } - results - } - pub fn background_highlights_in_range_for( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - theme: &Theme, - ) -> Vec<(Range, Color)> { - let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; - let Some((color_fetcher, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; - let color = color_fetcher(theme); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { - break; + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); + results.push((start..end, color)) } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, color)) } - results } @@ -7640,15 +7621,18 @@ impl Editor { display_snapshot: &DisplaySnapshot, count: usize, ) -> Vec> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; let Some((_, ranges)) = self.background_highlights .get(&TypeId::of::()) else { return vec![]; }; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7657,30 +7641,28 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let mut push_region = |start: Option, end: Option| { + let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { - results.push( - start_display.to_display_point(display_snapshot) - ..=end_display.to_display_point(display_snapshot), - ); + results.push(start_display..=end_display); } }; - let mut start_row: Option = None; - let mut end_row: Option = None; + let mut start_row: Option = None; + let mut end_row: Option = None; if ranges.len() > count { - return vec![]; + return Vec::new(); } for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let end = range.end.to_point(buffer); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); if let Some(current_row) = &end_row { - if end.row == current_row.row { + if end.row() == current_row.row() { continue; } } - let start = range.start.to_point(buffer); + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); if start_row.is_none() { assert_eq!(end_row, None); @@ -7689,7 +7671,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start.row > current_end.row + 1 { + if start.row() > current_end.row() + 1 { push_region(start_row, end_row); start_row = Some(start); end_row = Some(end); @@ -8133,6 +8115,19 @@ impl Editor { } } +fn document_to_inlay_range( + range: &DocumentRange, + snapshot: &DisplaySnapshot, +) -> Range { + match range { + DocumentRange::Text(text_range) => { + snapshot.anchor_to_inlay_offset(text_range.start) + ..snapshot.anchor_to_inlay_offset(text_range.end) + } + DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end, + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 8d7b6aa5c98700741dd5d17370bd9e3f97024a86..033525395e17f0db865fff79c225338f282ec889 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -225,6 +225,7 @@ impl<'a> EditorTestContext<'a> { .map(|h| h.1.clone()) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect() }); From bcaff0a18a924412348dbf4dfee7fbb2c6176856 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 15:14:25 +0300 Subject: [PATCH 070/142] Propagate inlay background highlights to data storage --- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/display_map/tab_map.rs | 1 - crates/editor/src/editor.rs | 18 +++- crates/editor/src/element.rs | 87 ++++++++++++---- crates/editor/src/hover_popover.rs | 109 +++++++++++++++++++-- crates/project/src/lsp_command.rs | 1 - 6 files changed, 185 insertions(+), 33 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 5094d2fab9595b613f183f587de163d1b039df51..56df722f525d8b3909bc60fccbd3c873dcfd1597 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1596,7 +1596,7 @@ mod tests { .map(|range| { buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) }) - // TODO add inlay highlight tests + // TODO kb add inlay highlight tests .map(DocumentRange::Text) .collect::>(); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index cae9ccc91f4b0a9bb2c9d208f96b82157ce5a176..fcdef17a8b65f250a0e355ad10731cfdf8c3350b 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -223,7 +223,6 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - // TODO kb extract into one struct? text_highlights: Option<&'a TextHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2089fe0f5fa9ce839df6519f27bbea6ac51d21fa..b21da05958fe2edbe75eb21b727cd3acba18135e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7511,6 +7511,22 @@ impl Editor { cx.notify(); } + pub fn highlight_inlay_background( + &mut self, + ranges: Vec, + color_fetcher: fn(&Theme) -> Color, + cx: &mut ViewContext, + ) { + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + ), + ); + cx.notify(); + } + pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, @@ -7932,7 +7948,6 @@ impl Editor { Some( ranges .iter() - // TODO kb mark inlays too .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) @@ -8400,7 +8415,6 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; - // TODO kb mark inlays too let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2be5105b33493724e43d3a65b4a631b3551f2139..e9a154ddb0185fb0f4485a5d72765a0cac0dcd61 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8,8 +8,8 @@ use crate::{ editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ - hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, - MIN_POPOVER_LINE_HEIGHT, + hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP, + MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, @@ -43,7 +43,8 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState, + HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, + Location, LocationLink, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -1860,8 +1861,7 @@ fn update_inlay_link_and_hover_points( None }; if let Some(hovered_offset) = hovered_offset { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); + let snapshot = editor.buffer().read(cx).snapshot(cx); let previous_valid_anchor = snapshot.anchor_at( point_for_position .previous_valid @@ -1885,15 +1885,14 @@ fn update_inlay_link_and_hover_points( .max_by_key(|hint| hint.id) { let inlay_hint_cache = editor.inlay_hint_cache(); - if let Some(cached_hint) = - inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) - { + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( buffer_id, - previous_valid_anchor.excerpt_id, + excerpt_id, hovered_hint.id, cx, ); @@ -1902,9 +1901,33 @@ fn update_inlay_link_and_hover_points( ResolveState::Resolved => { match cached_hint.label { project::InlayHintLabel::String(_) => { - if cached_hint.tooltip.is_some() { - dbg!(&cached_hint.tooltip); // TODO kb - // hover_at_point = Some(hovered_offset); + if let Some(tooltip) = cached_hint.tooltip { + hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: hint_start_offset, + highlight_end: hint_end_offset, + }, + }, + cx, + ); + hover_updated = true; } } project::InlayHintLabel::LabelParts(label_parts) => { @@ -1915,15 +1938,41 @@ fn update_inlay_link_and_hover_points( hovered_offset, ) { - if hovered_hint_part.tooltip.is_some() { - dbg!(&hovered_hint_part.tooltip); // TODO kb - // hover_at_point = Some(hovered_offset); + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; } if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = cached_hint - .position - .buffer_id - .and_then(|buffer_id| buffer.buffer(buffer_id)) + if let Some(buffer) = + cached_hint.position.buffer_id.and_then(|buffer_id| { + editor.buffer().read(cx).buffer(buffer_id) + }) { go_to_definition_updated = true; update_go_to_definition_link( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e4509a765cb82583188188eebd8a061e48feaf86..8e8babb44aa3afe47b90a2ef647ab70cbec148c8 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,8 @@ use crate::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, - EditorSnapshot, EditorStyle, RangeToAnchorExt, + display_map::{InlayOffset, ToDisplayPoint}, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, + ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; use gpui::{ @@ -46,6 +48,84 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC } } +pub struct InlayHover { + pub excerpt: ExcerptId, + pub triggered_from: InlayOffset, + pub range: InlayRange, + pub tooltip: HoverBlock, +} + +pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { + if settings::get::(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } + + let Some(project) = editor.project.clone() else { + return; + }; + + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let DocumentRange::Inlay(range) = symbol_range { + if (range.highlight_start..=range.highlight_end) + .contains(&inlay_hover.triggered_from) + { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } + + let snapshot = editor.snapshot(cx); + // Don't request again if the location is the same as the previous request + if let Some(triggered_from) = editor.hover_state.triggered_from { + if inlay_hover.triggered_from + == snapshot + .display_snapshot + .anchor_to_inlay_offset(triggered_from) + { + return; + } + } + + let task = cx.spawn(|this, mut cx| { + async move { + cx.background() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; + + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: DocumentRange::Inlay(inlay_hover.range), + blocks: vec![inlay_hover.tooltip], + language: None, + rendered_content: None, + }; + + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.editor.hover_popover.highlight, + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); + } +} + /// Hides the type information popup. /// Triggered by the `Hover` action when the cursor is not over a symbol or when the /// selections changed. @@ -110,8 +190,13 @@ fn show_hover( if !ignore_timeout { if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if symbol_range - .to_offset(&snapshot.buffer_snapshot) - .contains(&multibuffer_offset) + .as_text_range() + .map(|range| { + range + .to_offset(&snapshot.buffer_snapshot) + .contains(&multibuffer_offset) + }) + .unwrap_or(false) { // Hover triggered from same location as last time. Don't show again. return; @@ -219,7 +304,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: range, + symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, rendered_content: None, @@ -227,10 +312,13 @@ fn show_hover( }); this.update(&mut cx, |this, cx| { - if let Some(hover_popover) = hover_popover.as_ref() { + if let Some(symbol_range) = hover_popover + .as_ref() + .and_then(|hover_popover| hover_popover.symbol_range.as_text_range()) + { // Highlight the selected symbol using a background highlight this.highlight_background::( - vec![hover_popover.symbol_range.clone()], + vec![symbol_range], |theme| theme.editor.hover_popover.highlight, cx, ); @@ -497,7 +585,10 @@ impl HoverState { .or_else(|| { self.info_popover .as_ref() - .map(|info_popover| &info_popover.symbol_range.start) + .map(|info_popover| match &info_popover.symbol_range { + DocumentRange::Text(range) => &range.start, + DocumentRange::Inlay(range) => &range.inlay_position, + }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -522,7 +613,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - pub symbol_range: Range, + symbol_range: DocumentRange, pub blocks: Vec, language: Option>, rendered_content: Option, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 20bb302b5b8963e996a1da47564f26ad191b56ad..c057718bf3abdf6bde3a222b0fea32f7f727b36a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2126,7 +2126,6 @@ impl InlayHints { }) } - // TODO kb instead, store all LSP data inside the project::InlayHint? pub fn project_to_lsp_hint( hint: InlayHint, project: &ModelHandle, From dcf570bb03d3ce7e6290c161c390ddbdd19b6a20 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 19:43:05 +0300 Subject: [PATCH 071/142] Fix resolve status conversion --- crates/editor/src/display_map.rs | 4 +- crates/editor/src/display_map/block_map.rs | 4 +- crates/editor/src/display_map/fold_map.rs | 4 +- crates/editor/src/display_map/inlay_map.rs | 116 ++++++++++----------- crates/editor/src/display_map/tab_map.rs | 4 +- crates/editor/src/display_map/wrap_map.rs | 4 +- crates/editor/src/editor.rs | 3 +- crates/project/src/lsp_command.rs | 4 +- crates/project/src/project.rs | 2 +- 9 files changed, 72 insertions(+), 73 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 34e877a4483e70169b92d8a075d38bc116c0cef2..611866bcadeaef851ba081434fadc04a2d3031ae 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -471,14 +471,14 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 8577e928199a1af11612a813bd1c0648a2f409d4..741507004cc9bc0064ba682701310b832111438f 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -589,7 +589,7 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); @@ -623,7 +623,7 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_chunk: Default::default(), diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index dcbc156c47f1dbc65d10c013095996898d4ca32b..d5473027a6b0145bad28f21c1e91ce7491f9eb63 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -652,7 +652,7 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,7 +675,7 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), inlay_chunk: None, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 56df722f525d8b3909bc60fccbd3c873dcfd1597..748e1f0cd8dcb85cf218d314c2c6b2205920f097 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -997,74 +997,74 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); - let empty_text_highlights = TextHighlights::default(); - let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); - let mut highlight_endpoints = Vec::new(); - if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self - .buffer - .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); - let transform_start = self.to_inlay_offset(transform_start.to_offset(&self.buffer)); - - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; - let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - - for (tag, text_highlights) in text_highlights.iter() { - let style = text_highlights.0; - let ranges = &text_highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = self - .document_to_inlay_range(probe) - .end - .cmp(&transform_start); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, + if let Some(text_highlights) = text_highlights { + if !text_highlights.is_empty() { + while cursor.start().0 < range.end { + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); + let transform_start = + self.to_inlay_offset(transform_start.to_offset(&self.buffer)); + + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) }; - for range in &ranges[start_ix..] { - let range = self.document_to_inlay_range(range); - if range.start.cmp(&transform_end).is_ge() { - break; - } + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); + + for (tag, text_highlights) in text_highlights.iter() { + let style = text_highlights.0; + let ranges = &text_highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { + break; + } - highlight_endpoints.push(HighlightEndpoint { - offset: range.start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.end, - is_start: false, - tag: *tag, - style, - }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.end, + is_start: false, + tag: *tag, + style, + }); + } } - } - cursor.next(&()); + cursor.next(&()); + } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); } - highlight_endpoints.sort(); - cursor.seek(&range.start, Bias::Right, &()); } let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1078,7 +1078,7 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index fcdef17a8b65f250a0e355ad10731cfdf8c3350b..2cf0471b37889a5cf5d3db26cfe3d1de91dc8e20 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -224,7 +224,7 @@ impl TabSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = @@ -246,7 +246,7 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_column, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index d4b52d5893328e5e5abf50c20c842cf8c4a5622b..f3600936f9bf77df6773ad14fd39f4e465398e15 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -576,7 +576,7 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); @@ -595,7 +595,7 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_chunk: Default::default(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b21da05958fe2edbe75eb21b727cd3acba18135e..785d43f0b639efa4d23e84eacef197df3d2e150f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4882,7 +4882,6 @@ impl Editor { if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; to_insert = &clipboard_text[start_offset..end_offset]; - dbg!(start_offset, end_offset, &clipboard_text, &to_insert); entire_line = clipboard_selection.is_entire_line; start_offset = end_offset + 1; original_indent_column = @@ -7586,7 +7585,7 @@ impl Editor { let right_position = right_position.clone(); ranges[start_ix..].iter().take_while(move |range| { - document_to_inlay_range(range, &display_snapshot) + document_to_inlay_range(range, display_snapshot) .start .cmp(&right_position) .is_le() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c057718bf3abdf6bde3a222b0fea32f7f727b36a..9f7799c555940607a78b4faedd63bf89b16ddada 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1935,8 +1935,9 @@ impl InlayHints { pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::Resolved => (0, None), ResolveState::CanResolve(server_id, resolve_data) => ( - 0, + 1, resolve_data .map(|json_data| { serde_json::to_string(&json_data) @@ -1947,7 +1948,6 @@ impl InlayHints { value, }), ), - ResolveState::Resolved => (1, None), ResolveState::Resolving => (2, None), }; let resolve_state = Some(proto::ResolveState { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fbdbd04664fa4a76ebfa9e7992fd35987da8ad2f..1fe307eec2bc66f2b1f484fc98a984af7d14234c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5091,7 +5091,7 @@ impl Project { InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) .await .map(Some) - .context("inlay hints proto response conversion") + .context("inlay hints proto resolve response conversion") } None => Ok(None), } From 7cd60d6afb95d777644a27e9e3096129c19771c0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 20:22:38 +0300 Subject: [PATCH 072/142] Simplify and restore client resolve capabilities --- crates/editor/src/display_map/inlay_map.rs | 4 ---- crates/editor/src/editor.rs | 23 ++++++++++++++-------- crates/lsp/src/lsp.rs | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 748e1f0cd8dcb85cf218d314c2c6b2205920f097..1b65719448a0c74df3f747b5e0e35ef1e605303a 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -392,10 +392,6 @@ impl InlayPoint { pub fn row(self) -> u32 { self.0.row } - - pub fn column(self) -> u32 { - self.0.column - } } impl InlayMap { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 785d43f0b639efa4d23e84eacef197df3d2e150f..681e1d48b277a60ef0bd4d53cfc9726caed9fb4a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7656,13 +7656,16 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let mut push_region = |start: Option, end: Option| { + let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { - results.push(start_display..=end_display); + results.push( + start_display.to_display_point(display_snapshot) + ..=end_display.to_display_point(display_snapshot), + ); } }; - let mut start_row: Option = None; - let mut end_row: Option = None; + let mut start_row: Option = None; + let mut end_row: Option = None; if ranges.len() > count { return Vec::new(); } @@ -7671,13 +7674,17 @@ impl Editor { if range.start.cmp(&search_range.end).is_ge() { break; } - let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); + let end = display_snapshot + .inlay_offset_to_display_point(range.end, Bias::Right) + .to_point(display_snapshot); if let Some(current_row) = &end_row { - if end.row() == current_row.row() { + if end.row == current_row.row { continue; } } - let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let start = display_snapshot + .inlay_offset_to_display_point(range.start, Bias::Left) + .to_point(display_snapshot); if start_row.is_none() { assert_eq!(end_row, None); @@ -7686,7 +7693,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start.row() > current_end.row() + 1 { + if start.row > current_end.row + 1 { push_region(start_row, end_row); start_row = Some(start); end_row = Some(end); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 78c858a90c46e2af349027ed3f4f361fe8805752..e0ae64d8069c08b12e11b8b12155892dc974ae0d 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() From 852427e87b0f6c7b56b7dca38536a255c25aef86 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:08:08 +0300 Subject: [PATCH 073/142] Use inlay highlights in randomized tests --- crates/editor/src/display_map/inlay_map.rs | 57 +++++++++++++--------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 1b65719448a0c74df3f747b5e0e35ef1e605303a..25b8d3aef6a28b959a6092e1cfba4adf031dd125 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1144,13 +1144,12 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { #[cfg(test)] mod tests { use super::*; - use crate::{InlayId, MultiBuffer}; + use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer}; use gpui::AppContext; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; - use sum_tree::TreeMap; use text::Patch; use util::post_inc; @@ -1579,28 +1578,6 @@ mod tests { let mut buffer_snapshot = buffer.read(cx).snapshot(cx); let mut next_inlay_id = 0; log::info!("buffer text: {:?}", buffer_snapshot.text()); - - let mut highlights = TreeMap::default(); - let highlight_count = rng.gen_range(0_usize..10); - let mut highlight_ranges = (0..highlight_count) - .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) - .collect::>(); - highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); - log::info!("highlighting ranges {:?}", highlight_ranges); - let highlight_ranges = highlight_ranges - .into_iter() - .map(|range| { - buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) - }) - // TODO kb add inlay highlight tests - .map(DocumentRange::Text) - .collect::>(); - - highlights.insert( - Some(TypeId::of::<()>()), - Arc::new((HighlightStyle::default(), highlight_ranges)), - ); - let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); for _ in 0..operations { let mut inlay_edits = Patch::default(); @@ -1663,6 +1640,38 @@ mod tests { ); } + let mut highlights = TextHighlights::default(); + let highlight_count = rng.gen_range(0_usize..10); + let mut highlight_ranges = (0..highlight_count) + .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) + .collect::>(); + highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting ranges {:?}", highlight_ranges); + let highlight_ranges = if rng.gen_bool(0.5) { + highlight_ranges + .into_iter() + .map(|range| InlayRange { + inlay_position: buffer_snapshot.anchor_before(range.start), + highlight_start: inlay_snapshot.to_inlay_offset(range.start), + highlight_end: inlay_snapshot.to_inlay_offset(range.end), + }) + .map(DocumentRange::Inlay) + .collect::>() + } else { + highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end) + }) + .map(DocumentRange::Text) + .collect::>() + }; + highlights.insert( + Some(TypeId::of::<()>()), + Arc::new((HighlightStyle::default(), highlight_ranges)), + ); + for _ in 0..5 { let mut end = rng.gen_range(0..=inlay_snapshot.len().0); end = expected_text.clip_offset(end, Bias::Right); From 3c55c933d4064d06f3d5d3e2cf8336c3deefaf40 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:37:53 +0300 Subject: [PATCH 074/142] Be more lenient with hint resolution, always return some hint --- crates/editor/src/inlay_hint_cache.rs | 38 ++++++++++++--------------- crates/project/src/project.rs | 21 +++++++-------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 52a4039a76e6b31dd9e46f13de627591e5b916f8..d4325e13d94ad1e173ed34b5adeae34a49fcaa25 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -449,30 +449,26 @@ impl InlayHintCache { }) })?; if let Some(resolved_hint_task) = resolved_hint_task { - if let Some(mut resolved_hint) = - resolved_hint_task.await.context("hint resolve task")? - { - editor.update(&mut cx, |editor, _| { - if let Some(excerpt_hints) = - editor.inlay_hint_cache.hints.get(&excerpt_id) + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard - .hints - .iter_mut() - .find(|(hint_id, _)| hint_id == &id) - .map(|(_, hint)| hint) - { - if cached_hint.resolve_state == ResolveState::Resolving - { - resolved_hint.resolve_state = - ResolveState::Resolved; - *cached_hint = resolved_hint; - } + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; } } - })?; - } + } + })?; } anyhow::Ok(()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1fe307eec2bc66f2b1f484fc98a984af7d14234c..0bbb61dfcb44065b105ac331a414f54ae12ed17d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5033,7 +5033,7 @@ impl Project { buffer_handle: ModelHandle, server_id: LanguageServerId, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task> { if self.is_local() { let buffer = buffer_handle.read(cx); let (_, lang_server) = if let Some((adapter, server)) = @@ -5041,7 +5041,7 @@ impl Project { { (adapter.clone(), server.clone()) } else { - return Task::ready(Ok(None)); + return Task::ready(Ok(hint)); }; let can_resolve = lang_server .capabilities() @@ -5050,7 +5050,7 @@ impl Project { .and_then(|options| options.resolve_provider) .unwrap_or(false); if !can_resolve { - return Task::ready(Ok(None)); + return Task::ready(Ok(hint)); } let buffer_snapshot = buffer.snapshot(); @@ -5071,7 +5071,7 @@ impl Project { &mut cx, ) .await?; - Ok(Some(resolved_hint)) + Ok(resolved_hint) }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); @@ -5079,7 +5079,7 @@ impl Project { project_id, buffer_id: buffer_handle.read(cx).remote_id(), language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint, cx)), + hint: Some(InlayHints::project_to_proto_hint(hint.clone(), cx)), }; cx.spawn(|project, mut cx| async move { let response = client @@ -5090,10 +5090,9 @@ impl Project { Some(resolved_hint) => { InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) .await - .map(Some) .context("inlay hints proto resolve response conversion") } - None => Ok(None), + None => Ok(hint), } }) } else { @@ -6917,7 +6916,7 @@ impl Project { .and_then(|buffer| buffer.upgrade(cx)) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; - let resolved_hint = this + let response_hint = this .update(&mut cx, |project, cx| { project.resolve_inlay_hint( hint, @@ -6927,11 +6926,11 @@ impl Project { ) }) .await - .context("inlay hints fetch")? - .map(|hint| cx.read(|cx| InlayHints::project_to_proto_hint(hint, cx))); + .context("inlay hints fetch")?; + let resolved_hint = cx.read(|cx| InlayHints::project_to_proto_hint(response_hint, cx)); Ok(proto::ResolveInlayHintResponse { - hint: resolved_hint, + hint: Some(resolved_hint), }) } From abd2d012b1914fcea6830116a5dea88fbd69963d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:50:53 +0300 Subject: [PATCH 075/142] Properly binary search cached inlay hints --- crates/editor/src/inlay_hint_cache.rs | 38 ++++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d4325e13d94ad1e173ed34b5adeae34a49fcaa25..71b65de676a0503cc9461bc1401eb5ca236e36a4 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -714,13 +714,21 @@ fn calculate_hint_updates( probe.1.position.cmp(&new_hint.position, buffer_snapshot) }) { Ok(ix) => { - let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix]; - if cached_hint == &new_hint { - excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); - false - } else { - true + let mut missing_from_cache = true; + for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] { + if new_hint + .position + .cmp(&cached_hint.position, buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint == &new_hint { + excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); + missing_from_cache = false; + } } + missing_from_cache } Err(_) => true, } @@ -820,11 +828,21 @@ fn apply_hint_update( .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) + let mut insert_position = Some(i); + for (_, cached_hint) in &cached_hints[i..] { + if new_hint + .position + .cmp(&cached_hint.position, &buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint.text() == new_hint.text() { + insert_position = None; + break; + } } + insert_position } Err(i) => Some(i), }; From f19c659ed6aa63e56effddfcf7d3952d325854da Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 11:45:07 +0300 Subject: [PATCH 076/142] Add link_go_to_definition test for inlays --- crates/editor/src/element.rs | 232 +----------- crates/editor/src/hover_popover.rs | 23 +- crates/editor/src/inlay_hint_cache.rs | 14 +- crates/editor/src/link_go_to_definition.rs | 413 ++++++++++++++++++++- 4 files changed, 447 insertions(+), 235 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e9a154ddb0185fb0f4485a5d72765a0cac0dcd61..3ba807308c6718e2fcbf7e8edf0f9c9f9d08824e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,16 +4,16 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ - hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP, - MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, + hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, + MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, InlayRange, + update_inlay_link_and_hover_points, GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -43,8 +43,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, - Location, LocationLink, ProjectPath, ResolveState, + ProjectPath, }; use smallvec::SmallVec; use std::{ @@ -478,10 +477,11 @@ impl EditorElement { } None => { update_inlay_link_and_hover_points( - position_map, + &position_map.snapshot, point_for_position, editor, - (cmd, shift), + cmd, + shift, cx, ); } @@ -1835,214 +1835,6 @@ impl EditorElement { } } -fn update_inlay_link_and_hover_points( - position_map: &PositionMap, - point_for_position: PointForPosition, - editor: &mut Editor, - (cmd_held, shift_held): (bool, bool), - cx: &mut ViewContext<'_, '_, Editor>, -) { - let hint_start_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { - Some( - position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), - ) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) - } else { - None - }; - if let Some(hovered_offset) = hovered_offset { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let previous_valid_anchor = snapshot.anchor_at( - point_for_position - .previous_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Left, - ); - let next_valid_anchor = snapshot.anchor_at( - point_for_position - .next_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Right, - ); - - let mut go_to_definition_updated = false; - let mut hover_updated = false; - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - let inlay_hint_cache = editor.inlay_hint_cache(); - let excerpt_id = previous_valid_anchor.excerpt_id; - if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { - match cached_hint.resolve_state { - ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = previous_valid_anchor.buffer_id { - inlay_hint_cache.spawn_hint_resolve( - buffer_id, - excerpt_id, - hovered_hint.id, - cx, - ); - } - } - ResolveState::Resolved => { - match cached_hint.label { - project::InlayHintLabel::String(_) => { - if let Some(tooltip) = cached_hint.tooltip { - hover_at_inlay( - editor, - InlayHover { - excerpt: excerpt_id, - tooltip: match tooltip { - InlayHintTooltip::String(text) => HoverBlock { - text, - kind: HoverBlockKind::PlainText, - }, - InlayHintTooltip::MarkupContent(content) => { - HoverBlock { - text: content.value, - kind: content.kind, - } - } - }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: hint_start_offset, - highlight_end: hint_end_offset, - }, - }, - cx, - ); - hover_updated = true; - } - } - project::InlayHintLabel::LabelParts(label_parts) => { - if let Some((hovered_hint_part, part_range)) = - find_hovered_hint_part( - label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) - { - if let Some(tooltip) = hovered_hint_part.tooltip { - hover_at_inlay( - editor, - InlayHover { - excerpt: excerpt_id, - tooltip: match tooltip { - InlayHintLabelPartTooltip::String(text) => { - HoverBlock { - text, - kind: HoverBlockKind::PlainText, - } - } - InlayHintLabelPartTooltip::MarkupContent( - content, - ) => HoverBlock { - text: content.value, - kind: content.kind, - }, - }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - }, - cx, - ); - hover_updated = true; - } - if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = - cached_hint.position.buffer_id.and_then(|buffer_id| { - editor.buffer().read(cx).buffer(buffer_id) - }) - { - go_to_definition_updated = true; - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::InlayHint( - InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - LocationLink { - origin: Some(Location { - buffer, - range: cached_hint.position - ..cached_hint.position, - }), - target: location, - }, - ), - cmd_held, - shift_held, - cx, - ); - } - } - } - } - }; - } - ResolveState::Resolving => {} - } - } - } - - if !go_to_definition_updated { - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::None, - cmd_held, - shift_held, - cx, - ); - } - if !hover_updated { - hover_at(editor, None, cx); - } - } -} - -fn find_hovered_hint_part( - label_parts: Vec, - hint_range: Range, - hovered_offset: InlayOffset, -) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { - let mut hovered_character = (hovered_offset - hint_range.start).0; - let mut part_start = hint_range.start; - for part in label_parts { - let part_len = part.value.chars().count(); - if hovered_character >= part_len { - hovered_character -= part_len; - part_start.0 += part_len; - } else { - return Some((part, part_start..InlayOffset(part_start.0 + part_len))); - } - } - } - None -} - struct HighlightedChunk<'a> { chunk: &'a str, style: Option, @@ -2871,12 +2663,12 @@ struct PositionMap { snapshot: EditorSnapshot, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct PointForPosition { - previous_valid: DisplayPoint, + pub previous_valid: DisplayPoint, pub next_valid: DisplayPoint, - exact_unclipped: DisplayPoint, - column_overshoot_after_line_end: u32, + pub exact_unclipped: DisplayPoint, + pub column_overshoot_after_line_end: u32, } impl PointForPosition { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 8e8babb44aa3afe47b90a2ef647ab70cbec148c8..6eae470badc812f7d4889e6ff705244b46ae2300 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -13,7 +13,7 @@ use gpui::{ AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; -use project::{HoverBlock, HoverBlockKind, Project}; +use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -55,6 +55,27 @@ pub struct InlayHover { pub tooltip: HoverBlock, } +pub fn find_hovered_hint_part( + label_parts: Vec, + hint_range: Range, + hovered_offset: InlayOffset, +) -> Option<(InlayHintLabelPart, Range)> { + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character >= part_len { + hovered_character -= part_len; + part_start.0 += part_len; + } else { + return Some((part, part_start..InlayOffset(part_start.0 + part_len))); + } + } + } + None +} + pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { if settings::get::(cx).hover_popover_enabled { if editor.pending_rename.is_some() { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 71b65de676a0503cc9461bc1401eb5ca236e36a4..b0c7d9e0f1a7c5bd64758126b199425f59b77151 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -904,7 +904,7 @@ fn apply_hint_update( } #[cfg(test)] -mod tests { +pub mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use crate::{ @@ -2989,15 +2989,11 @@ all hints should be invalidated and requeried for all of its visible excerpts" ("/a/main.rs", editor, fake_server) } - fn cached_hint_labels(editor: &Editor) -> Vec { + pub fn cached_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 (_, inlay) in &excerpt_hints.read().hints { + labels.push(inlay.text()); } } @@ -3005,7 +3001,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" labels } - fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { let mut hints = editor .visible_inlay_hints(cx) .into_iter() diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 30f273065a54623edc084534e60e18869db00187..ea22ea5eae15ee686067d4f1119cb6edd0a5ac0a 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,10 +1,15 @@ use crate::{ - display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, - EditorSnapshot, SelectPhase, + display_map::{DisplaySnapshot, InlayOffset}, + element::PointForPosition, + hover_popover::{self, InlayHover}, + Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; -use project::LocationLink; +use project::{ + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, + LocationLink, ResolveState, +}; use std::ops::Range; use util::TryFutureExt; @@ -23,7 +28,7 @@ pub enum GoToDefinitionTrigger { None, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InlayRange { pub inlay_position: Anchor, pub highlight_start: InlayOffset, @@ -140,6 +145,192 @@ pub fn update_go_to_definition_link( hide_link_definition(editor, cx); } +pub fn update_inlay_link_and_hover_points( + snapshot: &DisplaySnapshot, + point_for_position: PointForPosition, + editor: &mut Editor, + cmd_held: bool, + shift_held: bool, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + if let Some(hovered_offset) = hovered_offset { + let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let previous_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.previous_valid.to_point(snapshot), + Bias::Left, + ); + let next_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.next_valid.to_point(snapshot), + Bias::Right, + ); + + let mut go_to_definition_updated = false; + let mut hover_updated = false; + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position + .cmp(&previous_valid_anchor, &buffer_snapshot) + .is_lt() + }) + .take_while(|hint| { + hint.position + .cmp(&next_valid_anchor, &buffer_snapshot) + .is_le() + }) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { + match cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + match cached_hint.label { + project::InlayHintLabel::String(_) => { + if let Some(tooltip) = cached_hint.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: hint_start_offset, + highlight_end: hint_end_offset, + }, + }, + cx, + ); + hover_updated = true; + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some((hovered_hint_part, part_range)) = + hover_popover::find_hovered_hint_part( + label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) + { + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; + } + if let Some(location) = hovered_hint_part.location { + if let Some(buffer) = + cached_hint.position.buffer_id.and_then(|buffer_id| { + editor.buffer().read(cx).buffer(buffer_id) + }) + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + LocationLink { + origin: Some(Location { + buffer, + range: cached_hint.position + ..cached_hint.position, + }), + target: location, + }, + ), + cmd_held, + shift_held, + cx, + ); + } + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + + if !go_to_definition_updated { + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::None, + cmd_held, + shift_held, + cx, + ); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum LinkDefinitionKind { Symbol, @@ -391,14 +582,21 @@ fn go_to_fetched_definition_of_kind( #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + display_map::ToDisplayPoint, + editor_tests::init_test, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + test::editor_lsp_test_context::EditorLspTestContext, + }; use futures::StreamExt; use gpui::{ platform::{self, Modifiers, ModifiersChangedEvent}, View, }; use indoc::indoc; + use language::language_settings::InlayHintSettings; use lsp::request::{GotoDefinition, GotoTypeDefinition}; + use util::assert_set_eq; #[gpui::test] async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { @@ -853,4 +1051,209 @@ mod tests { "}); cx.foreground().run_until_parked(); } + + #[gpui::test] + async fn test_link_go_to_inlay(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 cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + cx.set_state(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "}); + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + fn main() { + let variable = TestStruct; + } + "}); + + let expected_uri = cx.buffer_lsp_url.clone(); + let inlay_label = ": TestStruct"; + cx.lsp + .handle_request::(move |params, _| { + let expected_uri = expected_uri.clone(); + async move { + assert_eq!(params.text_document.uri, expected_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: inlay_label.to_string(), + location: Some(lsp::Location { + uri: params.text_document.uri, + range: target_range, + }), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![inlay_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + fn main() { + let variable« »= TestStruct; + } + "}) + .get(0) + .cloned() + .unwrap(); + let hint_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (inlay_label.len() / 2) as u32, + } + }); + // Press cmd to trigger highlight + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let expected_highlight_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_ranges = vec![InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_highlight_start, + highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()), + }]; + assert_set_eq!(actual_ranges, expected_ranges); + }); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: false, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); + }); + + // Cmd+click without existing definition requests and jumps + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, hint_hover_position, false, cx); + }); + cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc! {" + struct «TestStructˇ»; + + fn main() { + let variable = TestStruct; + } + "}); + } } From e44516cc6c8197f401934394d939d89fd414a634 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:26:04 +0300 Subject: [PATCH 077/142] Add hover tests --- crates/editor/src/hover_popover.rs | 318 ++++++++++++++++++++- crates/editor/src/link_go_to_definition.rs | 12 +- crates/project/src/lsp_command.rs | 30 +- crates/project/src/project.rs | 8 +- 4 files changed, 344 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6eae470badc812f7d4889e6ff705244b46ae2300..19020b643ace141a38812cb95c1a462cbf1feadb 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -804,10 +804,17 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + link_go_to_definition::update_inlay_link_and_hover_points, + test::editor_lsp_test_context::EditorLspTestContext, + }; + use collections::BTreeSet; use gpui::fonts::Weight; use indoc::indoc; - use language::{Diagnostic, DiagnosticSet}; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -1243,4 +1250,311 @@ mod tests { editor }); } + + #[gpui::test] + async fn test_hover_inlay_label_parts(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 cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (entire_hint_label.find(new_type_label).unwrap() + + new_type_label.len() / 2) + as u32, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.foreground().run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + + let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_new_type_label_start, + highlight_end: InlayOffset( + expected_new_type_label_start.0 + new_type_label.len() + ), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for new type hint") + .text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (entire_hint_label.find(struct_label).unwrap() + + struct_label.len() / 2) + as u32, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_struct_label_start = + InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_struct_label_start, + highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for struct hint") + .text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ea22ea5eae15ee686067d4f1119cb6edd0a5ac0a..926c0d6ddeb588bf133d032e9492c2249e9711eb 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -41,7 +41,7 @@ pub enum TriggerPoint { InlayHint(InlayRange, LocationLink), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DocumentRange { Text(Range), Inlay(InlayRange), @@ -1096,7 +1096,7 @@ mod tests { "}); let expected_uri = cx.buffer_lsp_url.clone(); - let inlay_label = ": TestStruct"; + let hint_label = ": TestStruct"; cx.lsp .handle_request::(move |params, _| { let expected_uri = expected_uri.clone(); @@ -1105,7 +1105,7 @@ mod tests { Ok(Some(vec![lsp::InlayHint { position: hint_position, label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { - value: inlay_label.to_string(), + value: hint_label.to_string(), location: Some(lsp::Location { uri: params.text_document.uri, range: target_range, @@ -1125,7 +1125,7 @@ mod tests { .await; cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let expected_layers = vec![inlay_label.to_string()]; + let expected_layers = vec![hint_label.to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor)); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); }); @@ -1147,7 +1147,7 @@ mod tests { previous_valid: inlay_range.start.to_display_point(&snapshot), next_valid: inlay_range.end.to_display_point(&snapshot), exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (inlay_label.len() / 2) as u32, + column_overshoot_after_line_end: (hint_label.len() / 2) as u32, } }); // Press cmd to trigger highlight @@ -1185,7 +1185,7 @@ mod tests { let expected_ranges = vec![InlayRange { inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), highlight_start: expected_highlight_start, - highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()), + highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()), }]; assert_set_eq!(actual_ranges, expected_ranges); }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 9f7799c555940607a78b4faedd63bf89b16ddada..292f9a5226dfb4897c4faa94ab35e6108d4ec136 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -17,7 +17,7 @@ use language::{ CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; -use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; +use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { @@ -2213,6 +2213,22 @@ impl InlayHints { }, } } + + pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool { + capabilities + .inlay_hint_provider + .as_ref() + .and_then(|options| match options { + OneOf::Left(_is_supported) => None, + OneOf::Right(capabilities) => match capabilities { + lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider, + lsp::InlayHintServerCapabilities::RegistrationOptions(o) => { + o.inlay_hint_options.resolve_provider + } + }, + }) + .unwrap_or(false) + } } #[async_trait(?Send)] @@ -2269,14 +2285,10 @@ impl LspCommand for InlayHints { lsp_adapter.name.0.as_ref() == "typescript-language-server"; let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { - let resolve_state = match lsp_server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( - lsp::InlayHintOptions { - resolve_provider: Some(true), - .. - }, - ))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()), - _ => ResolveState::Resolved, + let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } else { + ResolveState::Resolved }; let project = project.clone(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0bbb61dfcb44065b105ac331a414f54ae12ed17d..c7765bf55a70b4829cf43575b7679db94ff44065 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5043,13 +5043,7 @@ impl Project { } else { return Task::ready(Ok(hint)); }; - let can_resolve = lang_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if !can_resolve { + if !InlayHints::can_resolve_inlays(lang_server.capabilities()) { return Task::ready(Ok(hint)); } From 8ed280a029c397b572b961db7aa892e1ed726562 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:30:07 +0300 Subject: [PATCH 078/142] Rebase fixes --- Cargo.lock | 2 +- crates/rpc/proto/zed.proto | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d669be5d9d2e7e154c5549c5d7eecfe99785e993..8197f883c0615e0e1293d16ec832c31e3c842031 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,7 +1220,7 @@ dependencies = [ "tempfile", "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index dbd700e264af7c1c3f89b014bbf9ad9476fd5536..ce47830af22e8203f33aaa86f3f953a86065ad94 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,8 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; - ResolveInlayHint resolve_inlay_hint = 131; - ResolveInlayHintResponse resolve_inlay_hint_response = 132; + ResolveInlayHint resolve_inlay_hint = 137; + ResolveInlayHintResponse resolve_inlay_hint_response = 138; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; From 0a18aa694f13f8cb0f5b3c015f317cb6c03d5dfe Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:46:39 +0300 Subject: [PATCH 079/142] Use stricter inlay range checks to avoid stuck highlights Often, hint ranges are separated by a single '<` char as in `Option>`. When moving the caret from left to right, avoid inclusive ranges to faster update the matching hint underline. --- crates/editor/src/hover_popover.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 19020b643ace141a38812cb95c1a462cbf1feadb..3ce936ae8275b03e4f75abe2cd39ae11f7938214 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -88,7 +88,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if let DocumentRange::Inlay(range) = symbol_range { - if (range.highlight_start..=range.highlight_end) + if (range.highlight_start..range.highlight_end) .contains(&inlay_hover.triggered_from) { // Hover triggered from same location as last time. Don't show again. From a63e1571dc0d143e3facf7fd61456a31d4250ce5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 01:10:01 +0300 Subject: [PATCH 080/142] Defer querying inlay hints for invisible editor ranges This way, only the visible part gets frequently queried on typing (and hint /refresh requests that follow), with queries for invisible ranges cancelled eagerly. --- crates/editor/src/inlay_hint_cache.rs | 312 +++++++++++++++++--------- 1 file changed, 203 insertions(+), 109 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b0c7d9e0f1a7c5bd64758126b199425f59b77151..b7f04c68b1d3c9216f0533943b37b980ada366cc 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2,6 +2,7 @@ use std::{ cmp, ops::{ControlFlow, Range}, sync::Arc, + time::Duration, }; use crate::{ @@ -9,6 +10,7 @@ use crate::{ }; use anyhow::Context; use clock::Global; +use futures::future; use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; @@ -17,7 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; -use sum_tree::Bias; +use text::ToOffset; use util::post_inc; pub struct InlayHintCache { @@ -81,7 +83,11 @@ impl InvalidationStrategy { } impl TasksForRanges { - fn new(sorted_ranges: Vec>, task: Task<()>) -> Self { + fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { + let mut sorted_ranges = Vec::new(); + sorted_ranges.extend(query_ranges.before_visible); + sorted_ranges.extend(query_ranges.visible); + sorted_ranges.extend(query_ranges.after_visible); Self { tasks: vec![task], sorted_ranges, @@ -91,82 +97,103 @@ impl TasksForRanges { fn update_cached_tasks( &mut self, buffer_snapshot: &BufferSnapshot, - query_range: Range, + query_ranges: QueryRanges, invalidate: InvalidationStrategy, - spawn_task: impl FnOnce(Vec>) -> Task<()>, + spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { - let ranges_to_query = match invalidate { + let query_ranges = 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 + let mut updated_ranges = query_ranges; + updated_ranges.before_visible = updated_ranges + .before_visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges.visible = updated_ranges + .visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges.after_visible = updated_ranges + .after_visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); self.sorted_ranges.clear(); - vec![query_range] + query_ranges } }; - if !ranges_to_query.is_empty() { - self.tasks.push(spawn_task(ranges_to_query)); + if !query_ranges.is_empty() { + self.tasks.push(spawn_task(query_ranges)); + } + } + + fn remove_cached_ranges( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + ) -> Vec> { + 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 } } @@ -515,11 +542,11 @@ fn spawn_new_update_tasks( } }; - let (multi_buffer_snapshot, Some(query_range)) = + let (multi_buffer_snapshot, Some(query_ranges)) = editor.buffer.update(cx, |multi_buffer, cx| { ( multi_buffer.snapshot(cx), - determine_query_range( + determine_query_ranges( multi_buffer, excerpt_id, &excerpt_buffer, @@ -535,10 +562,10 @@ fn spawn_new_update_tasks( invalidate, }; - let new_update_task = |fetch_ranges| { + let new_update_task = |query_ranges| { new_update_task( query, - fetch_ranges, + query_ranges, multi_buffer_snapshot, buffer_snapshot.clone(), Arc::clone(&visible_hints), @@ -551,57 +578,100 @@ fn spawn_new_update_tasks( hash_map::Entry::Occupied(mut o) => { o.get_mut().update_cached_tasks( &buffer_snapshot, - query_range, + query_ranges, invalidate, new_update_task, ); } hash_map::Entry::Vacant(v) => { v.insert(TasksForRanges::new( - vec![query_range.clone()], - new_update_task(vec![query_range]), + query_ranges.clone(), + new_update_task(query_ranges), )); } } } } -fn determine_query_range( +#[derive(Debug, Clone)] +struct QueryRanges { + before_visible: Vec>, + visible: Vec>, + after_visible: Vec>, +} + +impl QueryRanges { + fn is_empty(&self) -> bool { + self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty() + } +} + +fn determine_query_ranges( multi_buffer: &mut MultiBuffer, excerpt_id: ExcerptId, excerpt_buffer: &ModelHandle, excerpt_visible_range: Range, cx: &mut ModelContext<'_, MultiBuffer>, -) -> Option> { +) -> 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 snapshot = buffer.snapshot(); 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 + + let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end { + return None; } else { - Some(start..end) - } + vec![ + buffer.anchor_before(excerpt_visible_range.start) + ..buffer.anchor_after(excerpt_visible_range.end), + ] + }; + + let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); + let after_visible_range = if excerpt_visible_range.end == full_excerpt_range_end_offset { + Vec::new() + } else { + let after_range_end_offset = excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + vec![ + buffer.anchor_before(excerpt_visible_range.end) + ..buffer.anchor_after(after_range_end_offset), + ] + }; + + let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); + let before_visible_range = if excerpt_visible_range.start == full_excerpt_range_start_offset { + Vec::new() + } else { + let before_range_start_offset = excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range_start_offset); + vec![ + buffer.anchor_before(before_range_start_offset) + ..buffer.anchor_after(excerpt_visible_range.start), + ] + }; + + Some(QueryRanges { + before_visible: before_visible_range, + visible: visible_range, + after_visible: after_visible_range, + }) } +const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 300; + fn new_update_task( query: ExcerptQuery, - hint_fetch_ranges: Vec>, + query_ranges: QueryRanges, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, @@ -609,24 +679,48 @@ fn new_update_task( cx: &mut ViewContext<'_, '_, Editor>, ) -> 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(), - ) - })) + let fetch_and_update_hints = |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(), + ) + }; + let visible_range_update_results = future::join_all( + query_ranges + .visible + .into_iter() + .map(|visible_range| fetch_and_update_hints(visible_range)), + ) + .await; + for result in visible_range_update_results { + if let Err(e) = result { + error!("visible range inlay hint update task failed: {e:#}"); + } + } + + cx.background() + .timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )) .await; - for result in task_update_results { + let invisible_range_update_results = future::join_all( + query_ranges + .before_visible + .into_iter() + .chain(query_ranges.after_visible.into_iter()) + .map(|invisible_range| fetch_and_update_hints(invisible_range)), + ) + .await; + for result in invisible_range_update_results { if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); + error!("invisible range inlay hint update task failed: {e:#}"); } } }) @@ -1816,7 +1910,7 @@ pub mod tests { }); })); } - let _ = futures::future::join_all(edits).await; + let _ = future::join_all(edits).await; cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { From c10c3e2b544895b166d7d2f34f34f095a839f498 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 15:09:06 +0300 Subject: [PATCH 081/142] Only invalidate when doing first, visible range query --- crates/editor/src/inlay_hint_cache.rs | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b7f04c68b1d3c9216f0533943b37b980ada366cc..edd20bed3000f476b6be0c69923e3ba5b1a9e803 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -679,7 +679,7 @@ fn new_update_task( cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { cx.spawn(|editor, cx| async move { - let fetch_and_update_hints = |range| { + let fetch_and_update_hints = |invalidate, range| { fetch_and_update_hints( editor.clone(), multi_buffer_snapshot.clone(), @@ -687,17 +687,16 @@ fn new_update_task( Arc::clone(&visible_hints), cached_excerpt_hints.as_ref().map(Arc::clone), query, + invalidate, range, cx.clone(), ) }; - let visible_range_update_results = future::join_all( - query_ranges - .visible - .into_iter() - .map(|visible_range| fetch_and_update_hints(visible_range)), - ) - .await; + let visible_range_update_results = + future::join_all(query_ranges.visible.into_iter().map(|visible_range| { + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + })) + .await; for result in visible_range_update_results { if let Err(e) = result { error!("visible range inlay hint update task failed: {e:#}"); @@ -715,7 +714,7 @@ fn new_update_task( .before_visible .into_iter() .chain(query_ranges.after_visible.into_iter()) - .map(|invisible_range| fetch_and_update_hints(invisible_range)), + .map(|invisible_range| fetch_and_update_hints(false, invisible_range)), ) .await; for result in invisible_range_update_results { @@ -733,6 +732,7 @@ async fn fetch_and_update_hints( visible_hints: Arc>, cached_excerpt_hints: Option>>, query: ExcerptQuery, + invalidate: bool, fetch_range: Range, mut cx: gpui::AsyncAppContext, ) -> anyhow::Result<()> { @@ -761,7 +761,8 @@ async fn fetch_and_update_hints( .background() .spawn(async move { calculate_hint_updates( - query, + query.excerpt_id, + invalidate, backround_fetch_range, new_hints, &background_task_buffer_snapshot, @@ -788,7 +789,8 @@ async fn fetch_and_update_hints( } fn calculate_hint_updates( - query: ExcerptQuery, + excerpt_id: ExcerptId, + invalidate: bool, fetch_range: Range, new_excerpt_hints: Vec, buffer_snapshot: &BufferSnapshot, @@ -836,11 +838,11 @@ fn calculate_hint_updates( let mut remove_from_visible = Vec::new(); let mut remove_from_cache = HashSet::default(); - if query.invalidate.should_invalidate() { + if invalidate { remove_from_visible.extend( visible_hints .iter() - .filter(|hint| hint.position.excerpt_id == query.excerpt_id) + .filter(|hint| hint.position.excerpt_id == excerpt_id) .map(|inlay_hint| inlay_hint.id) .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), ); @@ -863,7 +865,7 @@ fn calculate_hint_updates( None } else { Some(ExcerptHintsUpdate { - excerpt_id: query.excerpt_id, + excerpt_id, remove_from_visible, remove_from_cache, add_to_cache, From 2b95f0580e340f54c129d0d4f88e6480298e5fb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 16:17:45 +0300 Subject: [PATCH 082/142] Fix the tests --- crates/editor/src/inlay_hint_cache.rs | 267 +++++++++++++++----------- 1 file changed, 158 insertions(+), 109 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index edd20bed3000f476b6be0c69923e3ba5b1a9e803..54ed8f8f1182610213b91c51d909adcd0a8f1df8 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -632,31 +632,38 @@ fn determine_query_ranges( }; let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); - let after_visible_range = if excerpt_visible_range.end == full_excerpt_range_end_offset { + let after_visible_range_start = excerpt_visible_range + .end + .saturating_add(1) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset { Vec::new() } else { - let after_range_end_offset = excerpt_visible_range - .end + let after_range_end_offset = after_visible_range_start .saturating_add(excerpt_visible_len) .min(full_excerpt_range_end_offset) .min(buffer.len()); vec![ - buffer.anchor_before(excerpt_visible_range.end) + buffer.anchor_before(after_visible_range_start) ..buffer.anchor_after(after_range_end_offset), ] }; let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); - let before_visible_range = if excerpt_visible_range.start == full_excerpt_range_start_offset { + let before_visible_range_end = excerpt_visible_range + .start + .saturating_sub(1) + .max(full_excerpt_range_start_offset); + let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset { Vec::new() } else { - let before_range_start_offset = excerpt_visible_range - .start + let before_range_start_offset = before_visible_range_end .saturating_sub(excerpt_visible_len) .max(full_excerpt_range_start_offset); vec![ buffer.anchor_before(before_range_start_offset) - ..buffer.anchor_after(excerpt_visible_range.start), + ..buffer.anchor_after(before_visible_range_end), ] }; @@ -667,7 +674,7 @@ fn determine_query_ranges( }) } -const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 300; +const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; fn new_update_task( query: ExcerptQuery, @@ -1001,7 +1008,7 @@ fn apply_hint_update( #[cfg(test)] pub mod tests { - use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use crate::{ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, @@ -1079,13 +1086,13 @@ pub mod tests { let mut edits_made = 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1104,13 +1111,13 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string(), "1".to_string()]; + let expected_hints = vec!["0".to_string(), "1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get new hints after an edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1129,13 +1136,13 @@ pub mod tests { edits_made += 1; cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get new hints after hint refresh/ request" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1189,13 +1196,13 @@ pub mod tests { let mut edits_made = 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1220,13 +1227,13 @@ pub mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should not update hints while the work task is running" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1244,13 +1251,13 @@ pub mod tests { edits_made += 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "New hints should be queried after the work task is done" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1363,13 +1370,13 @@ pub mod tests { .await; cx.foreground().run_until_parked(); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, 1, @@ -1421,13 +1428,13 @@ pub mod tests { .await; cx.foreground().run_until_parked(); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should have a separate verison, repeating Rust editor rules" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); @@ -1437,13 +1444,13 @@ pub mod tests { }); cx.foreground().run_until_parked(); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Rust inlay cache should change after the edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, 2, @@ -1451,13 +1458,13 @@ pub mod tests { ); }); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should not be affected by Rust editor changes" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); @@ -1467,23 +1474,23 @@ pub mod tests { }); cx.foreground().run_until_parked(); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Rust editor should not be affected by Markdown editor changes" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should also change independently" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); } @@ -2009,7 +2016,7 @@ pub mod tests { .downcast::() .unwrap(); let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); - let lsp_request_count = Arc::new(AtomicU32::new(0)); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); let closure_lsp_request_count = Arc::clone(&lsp_request_count); fake_server @@ -2023,10 +2030,9 @@ pub 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::Release) + 1; Ok(Some(vec![lsp::InlayHint { - position: query_start, + position: params.range.end, label: lsp::InlayHintLabel::String(i.to_string()), kind: None, text_edits: None, @@ -2063,28 +2069,51 @@ pub mod tests { }) } + // in large buffers, requests are made for more than visible range of a buffer. + // invisible parts are queried later, to avoid excessive requests on quick typing. + // wait the timeout needed to get all requests. + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); let initial_visible_range = editor_visible_range(&editor, cx); + let lsp_initial_visible_range = lsp::Range::new( + lsp::Position::new( + initial_visible_range.start.row, + initial_visible_range.start.column, + ), + lsp::Position::new( + initial_visible_range.end.row, + initial_visible_range.end.column, + ), + ); let expected_initial_query_range_end = - lsp::Position::new(initial_visible_range.end.row * 2, 1); - cx.foreground().run_until_parked(); + lsp::Position::new(initial_visible_range.end.row * 2, 2); + let mut expected_invisible_query_start = lsp_initial_visible_range.end; + expected_invisible_query_start.character += 1; editor.update(cx, |editor, cx| { 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!(ranges.len(), 2, + "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); + let visible_query_range = &ranges[0]; + assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); + assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); + let invisible_query_range = &ranges[1]; - assert_eq!(lsp_request_count.load(Ordering::Acquire), 1); - let expected_layers = vec!["1".to_string()]; + assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); + assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); + + let requests_count = lsp_request_count.load(Ordering::Acquire); + assert_eq!(requests_count, 2, "Visible + invisible request"); + let expected_hints = vec!["1".to_string(), "2".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should have hints from both LSP requests made for a big file" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); assert_eq!( - editor.inlay_hint_cache().version, 1, + editor.inlay_hint_cache().version, requests_count, "LSP queries should've bumped the cache version" ); }); @@ -2093,11 +2122,13 @@ pub mod tests { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); editor.scroll_screen(&ScrollAmount::Page(1.0), cx); }); - + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); 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() @@ -2124,26 +2155,28 @@ pub mod tests { lsp::Position::new( visible_range_after_scrolls.end.row + visible_line_count.ceil() as u32, - 0 + 1, ), "Second scroll should query one more screen down after the end of the visible range" ); + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); + let expected_hints = vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ]; 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, + expected_hints, 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!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, - 3, + lsp_requests, "Should update the cache for every LSP response with hints added" ); @@ -2157,6 +2190,9 @@ pub mod tests { s.select_ranges([selection_in_cached_range..selection_in_cached_range]) }); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); editor.update(cx, |_, _| { let ranges = lsp_request_ranges @@ -2165,33 +2201,43 @@ pub mod tests { .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); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); }); editor.update(cx, |editor, cx| { editor.handle_input("++++more text++++", cx); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { 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, + assert_eq!(ranges.len(), 3, + "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); + let visible_query_range = &ranges[0]; + let above_query_range = &ranges[1]; + let below_query_range = &ranges[2]; + assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, + "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); + assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, + "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); + assert!(above_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, + assert!(below_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, + assert!(above_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, + assert!(below_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::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), + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); + let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; + assert_eq!(expected_hints, 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, 4, "Should update the cache for every LSP response with hints added"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); }); } @@ -2402,19 +2448,19 @@ pub mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), ]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); }); editor.update(cx, |editor, cx| { @@ -2430,7 +2476,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2441,10 +2487,10 @@ pub mod tests { "other hint #1".to_string(), "other hint #2".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); }); @@ -2453,9 +2499,12 @@ pub mod tests { s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) }); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); let last_scroll_update_version = editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2469,11 +2518,11 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len()); - expected_layers.len() + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() }); editor.update(cx, |editor, cx| { @@ -2483,7 +2532,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2497,9 +2546,9 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); }); @@ -2512,7 +2561,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint(edited) #0".to_string(), "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), @@ -2523,15 +2572,15 @@ pub mod tests { "other hint(edited) #1".to_string(), ]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "After multibuffer edit, editor gets scolled back to the last selection; \ all hints should be invalidated and requeried for all of its visible excerpts" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let current_cache_version = editor.inlay_hint_cache().version; - let minimum_expected_version = last_scroll_update_version + expected_layers.len(); + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); assert!( current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" @@ -2872,9 +2921,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); 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)); + let expected_hints = vec!["1".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); } From 44c340b5f25dfd6bc2a27192e5f05a9e8c154101 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 17:33:17 +0300 Subject: [PATCH 083/142] Properly invalidate the hint cache --- crates/editor/src/inlay_hint_cache.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 54ed8f8f1182610213b91c51d909adcd0a8f1df8..b33b93a3487d36f9ffa939c0e4317664f299f9d3 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -785,6 +785,7 @@ async fn fetch_and_update_hints( editor, new_update, query, + invalidate, buffer_snapshot, multi_buffer_snapshot, cx, @@ -893,6 +894,7 @@ fn apply_hint_update( editor: &mut Editor, new_update: ExcerptHintsUpdate, query: ExcerptQuery, + invalidate: bool, buffer_snapshot: BufferSnapshot, multi_buffer_snapshot: MultiBufferSnapshot, cx: &mut ViewContext<'_, '_, Editor>, @@ -970,7 +972,7 @@ fn apply_hint_update( cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); drop(cached_excerpt_hints); - if query.invalidate.should_invalidate() { + if 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(); From 732af201dc599ef58a0ce650658775c6060ee58e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 09:59:16 -0700 Subject: [PATCH 084/142] Upgrade to rust 1.72 --- Dockerfile | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 77d011490e5821f282240af7d387b19f67a0edbe..208700f7fb5f25d19dc5e5cfd1477f11219c4391 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.71-bullseye as builder +FROM rust:1.72-bullseye as builder WORKDIR app COPY . . diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 50003020e9baf17e3e9e0b50babb19c354356e15..7ed8b98280ca394966621a55b80d3101ac6854c8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.71" +channel = "1.72" components = [ "rustfmt" ] targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] From 404f76739c3e032b83b0f69f13441f3b3a4025a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 10:11:32 -0700 Subject: [PATCH 085/142] Format let-else statements --- crates/ai/src/assistant.rs | 13 +++- crates/call/src/room.rs | 4 +- crates/collab/src/db.rs | 4 +- crates/collab/src/db/queries/rooms.rs | 6 +- .../src/tests/randomized_integration_tests.rs | 35 +++++++---- crates/collab_ui/src/channel_view.rs | 8 ++- .../src/collab_panel/channel_modal.rs | 9 +-- crates/editor/src/editor.rs | 60 ++++++++++--------- crates/editor/src/inlay_hint_cache.rs | 10 +++- crates/editor/src/items.rs | 20 +++++-- crates/editor/src/multi_buffer.rs | 4 +- .../gpui/src/keymap_matcher/keymap_context.rs | 4 +- crates/language/src/buffer.rs | 4 +- crates/language/src/syntax_map.rs | 12 +++- .../src/syntax_map/syntax_map_tests.rs | 8 ++- crates/language_tools/src/lsp_log.rs | 4 +- crates/project/src/lsp_command.rs | 14 ++++- crates/project/src/worktree.rs | 12 ++-- .../quick_action_bar/src/quick_action_bar.rs | 4 +- crates/search/src/project_search.rs | 4 +- crates/semantic_index/src/semantic_index.rs | 4 +- crates/settings/src/keymap_file.rs | 19 +++--- crates/vcs_menu/src/lib.rs | 32 +++++----- crates/vim/src/normal/paste.rs | 2 +- crates/vim/src/visual.rs | 7 ++- crates/workspace/src/pane_group.rs | 4 +- crates/workspace/src/workspace.rs | 8 ++- crates/zed/src/languages/python.rs | 4 +- 28 files changed, 210 insertions(+), 109 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 81299bbdc26f5001f407901893b7c8d3e0f1b166..2699bf40a088b38033af9244c4365d2c57d43dbf 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1128,7 +1128,9 @@ impl Conversation { stream: true, }; - let Some(api_key) = self.api_key.borrow().clone() else { continue }; + let Some(api_key) = self.api_key.borrow().clone() else { + continue; + }; let stream = stream_completion(api_key, cx.background().clone(), request); let assistant_message = self .insert_message_after( @@ -1484,7 +1486,9 @@ impl Conversation { }) { current_message = messages.next(); } - let Some(message) = current_message.as_ref() else { break }; + let Some(message) = current_message.as_ref() else { + break; + }; // Skip offsets that are in the same message. while offsets.peek().map_or(false, |offset| { @@ -1921,7 +1925,10 @@ impl ConversationEditor { let Some(panel) = workspace.panel::(cx) else { return; }; - let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::(cx)) else { + let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + else { return; }; diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 6f01b1d75789ce61d537be2d780f0dbb5960ad17..cc7445dbcc74ff620968c9ff5a2a99686bd800d9 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -644,7 +644,9 @@ impl Room { if let Some(participants) = remote_participants.log_err() { for (participant, user) in room.participants.into_iter().zip(participants) { - let Some(peer_id) = participant.peer_id else { continue }; + let Some(peer_id) = participant.peer_id else { + continue; + }; this.participant_user_ids.insert(participant.user_id); let old_projects = this diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 9c759f79a8dd47f4cc950809c7816f0204273372..888158188f35d82443e6a88b2237793d2dcdc016 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -249,7 +249,9 @@ impl Database { let mut tx = Arc::new(Some(tx)); let result = f(TransactionHandle(tx.clone())).await; let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else { - return Err(anyhow!("couldn't complete transaction because it's still in use"))?; + return Err(anyhow!( + "couldn't complete transaction because it's still in use" + ))?; }; Ok((tx, result)) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index a85d257187c2207b56b934540ba34c566eb1c77d..435e729fed38c2b5fcd4775e0de92ced74ee0b13 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -465,9 +465,9 @@ impl Database { let mut rejoined_projects = Vec::new(); for rejoined_project in &rejoin_room.rejoined_projects { let project_id = ProjectId::from_proto(rejoined_project.id); - let Some(project) = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? else { continue }; + let Some(project) = project::Entity::find_by_id(project_id).one(&*tx).await? else { + continue; + }; let mut worktrees = Vec::new(); let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 18fe6734cdda0dcb5194e518f06caf589751080e..3557843828cacb81b69b88d24514da6936b83139 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -121,7 +121,9 @@ async fn test_random_collaboration( let mut operation_channels = Vec::new(); loop { - let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { break }; + let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { + break; + }; applied.store(true, SeqCst); let did_apply = apply_server_operation( deterministic.clone(), @@ -224,7 +226,9 @@ async fn apply_server_operation( let client_ix = clients .iter() .position(|(client, cx)| client.current_user_id(cx) == removed_user_id); - let Some(client_ix) = client_ix else { return false }; + let Some(client_ix) = client_ix else { + return false; + }; let user_connection_ids = server .connection_pool .lock() @@ -1591,10 +1595,11 @@ impl TestPlan { 81.. => match self.rng.gen_range(0..100_u32) { // Add a worktree to a local project 0..=50 => { - let Some(project) = client - .local_projects() - .choose(&mut self.rng) - .cloned() else { continue }; + let Some(project) = + client.local_projects().choose(&mut self.rng).cloned() + else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let mut paths = client.fs().paths(false); paths.remove(0); @@ -1611,7 +1616,9 @@ impl TestPlan { // Add an entry to a worktree _ => { - let Some(project) = choose_random_project(client, &mut self.rng) else { continue }; + let Some(project) = choose_random_project(client, &mut self.rng) else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); let worktree = project.read_with(cx, |project, cx| { @@ -1645,7 +1652,9 @@ impl TestPlan { // Query and mutate buffers 60..=90 => { - let Some(project) = choose_random_project(client, &mut self.rng) else { continue }; + let Some(project) = choose_random_project(client, &mut self.rng) else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); @@ -1656,7 +1665,10 @@ impl TestPlan { .buffers_for_project(&project) .iter() .choose(&mut self.rng) - .cloned() else { continue }; + .cloned() + else { + continue; + }; let full_path = buffer .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); @@ -2026,7 +2038,10 @@ async fn simulate_client( client.app_state.languages.add(Arc::new(language)); while let Some(batch_id) = operation_rx.next().await { - let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break }; + let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) + else { + break; + }; applied.store(true, SeqCst); match apply_client_operation(&client, operation, &mut cx).await { Ok(()) => {} diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index bb1e840ffca40087519d324db5a2ae9a62a38222..a34f10b2db29f3f132e823fce27209d0a24a12c6 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -272,8 +272,12 @@ impl FollowableItem for ChannelView { state: &mut Option, cx: &mut AppContext, ) -> Option>>> { - let Some(proto::view::Variant::ChannelView(_)) = state else { return None }; - let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() }; + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; let open = ChannelView::open(state.channel_id, pane, workspace, cx); diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 0adf2806d72dc5c440ee08ec80a1331cdccc62cc..4c811a2df547dc78e0a602ae2002a4e9dbeb4e46 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -152,12 +152,9 @@ impl View for ChannelModal { let theme = &theme::current(cx).collab_panel.tabbed_modal; let mode = self.picker.read(cx).delegate().mode; - let Some(channel) = self - .channel_store - .read(cx) - .channel_for_id(self.channel_id) else { - return Empty::new().into_any() - }; + let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else { + return Empty::new().into_any(); + }; enum InviteMembers {} enum ManageMembers {} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 681e1d48b277a60ef0bd4d53cfc9726caed9fb4a..c5ff1f027da7faac89bbcbf596d501fffbeae2cb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6243,7 +6243,9 @@ impl Editor { ) { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_offsets_with(|snapshot, selection| { - let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else { + let Some(enclosing_bracket_ranges) = + snapshot.enclosing_bracket_ranges(selection.start..selection.end) + else { return; }; @@ -6255,7 +6257,8 @@ impl Editor { let close = close.to_inclusive(); let length = close.end() - open.start; let inside = selection.start >= open.end && selection.end <= *close.start(); - let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head()); + let in_bracket_range = open.to_inclusive().contains(&selection.head()) + || close.contains(&selection.head()); // If best is next to a bracket and current isn't, skip if !in_bracket_range && best_in_bracket_range { @@ -6270,19 +6273,21 @@ impl Editor { best_length = length; best_inside = inside; best_in_bracket_range = in_bracket_range; - best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) { - if inside { - open.end - } else { - open.start - } - } else { - if inside { - *close.start() + best_destination = Some( + if close.contains(&selection.start) && close.contains(&selection.end) { + if inside { + open.end + } else { + open.start + } } else { - *close.end() - } - }); + if inside { + *close.start() + } else { + *close.end() + } + }, + ); } if let Some(destination) = best_destination { @@ -6526,7 +6531,9 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { return }; + let Some(workspace) = self.workspace(cx) else { + return; + }; let buffer = self.buffer.read(cx); let head = self.selections.newest::(cx).head(); let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { @@ -6557,7 +6564,9 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { return }; + let Some(workspace) = self.workspace(cx) else { + return; + }; let pane = workspace.read(cx).active_pane().clone(); // If there is one definition, just open it directly if definitions.len() == 1 { @@ -7639,10 +7648,9 @@ impl Editor { let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let Some((_, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; + let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { + return vec![]; + }; let start_ix = match ranges.binary_search_by(|probe| { let cmp = document_to_inlay_range(probe, display_snapshot) @@ -7993,9 +8001,7 @@ impl Editor { suggestion_accepted: bool, cx: &AppContext, ) { - let Some(project) = &self.project else { - return - }; + let Some(project) = &self.project else { return }; // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension let file_extension = self @@ -8024,9 +8030,7 @@ impl Editor { file_extension: Option, cx: &AppContext, ) { - let Some(project) = &self.project else { - return - }; + let Some(project) = &self.project else { return }; // If None, we are in a file without an extension let file = self @@ -8127,7 +8131,9 @@ impl Editor { } } - let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; }; + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { + return; + }; cx.write_to_clipboard(ClipboardItem::new(lines)); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b33b93a3487d36f9ffa939c0e4317664f299f9d3..a4b91e826d06083a0979b6c4ffd45225d8fb41c2 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -341,7 +341,10 @@ impl InlayHintCache { shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { let Some(buffer) = shown_anchor .buffer_id - .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false }; + .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) + else { + return false; + }; let buffer_snapshot = buffer.read(cx).snapshot(); loop { match excerpt_cache.peek() { @@ -554,7 +557,10 @@ fn spawn_new_update_tasks( cx, ), ) - }) else { return; }; + }) + else { + return; + }; let query = ExcerptQuery { buffer_id, excerpt_id, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 30ed56af476547503d95afb563635ad9bddcbec6..d9998725922f5154e299bb3bd7a32b04ff18c2d2 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -55,8 +55,12 @@ impl FollowableItem for Editor { cx: &mut AppContext, ) -> Option>>> { let project = workspace.read(cx).project().to_owned(); - let Some(proto::view::Variant::Editor(_)) = state else { return None }; - let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; + let Some(proto::view::Variant::Editor(_)) = state else { + return None; + }; + let Some(proto::view::Variant::Editor(state)) = state.take() else { + unreachable!() + }; let client = project.read(cx).client(); let replica_id = project.read(cx).replica_id(); @@ -341,10 +345,16 @@ async fn update_editor_from_message( let mut insertions = message.inserted_excerpts.into_iter().peekable(); while let Some(insertion) = insertions.next() { - let Some(excerpt) = insertion.excerpt else { continue }; - let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue }; + let Some(excerpt) = insertion.excerpt else { + continue; + }; + let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { + continue; + }; let buffer_id = excerpt.buffer_id; - let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue }; + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { + continue; + }; let adjacent_excerpts = iter::from_fn(|| { let insertion = insertions.peek()?; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 9dd40af8981dab6d82ba4dda237ca46804575519..b9bf4f52a1c50e3b31aaf91da6586b9cfea7c958 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2756,7 +2756,9 @@ impl MultiBufferSnapshot { // Get the ranges of the innermost pair of brackets. let mut result: Option<(Range, Range)> = None; - let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; }; + let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { + return None; + }; for (open, close) in enclosing_bracket_ranges { let len = close.end - open.start; diff --git a/crates/gpui/src/keymap_matcher/keymap_context.rs b/crates/gpui/src/keymap_matcher/keymap_context.rs index fd60a8f4b5d385eb94b7edf0bfeb407a9dce8c20..d9c54dbc8e6fe71a7567ac4d6908a2d53689943d 100644 --- a/crates/gpui/src/keymap_matcher/keymap_context.rs +++ b/crates/gpui/src/keymap_matcher/keymap_context.rs @@ -67,7 +67,9 @@ impl KeymapContextPredicate { } pub fn eval(&self, contexts: &[KeymapContext]) -> bool { - let Some(context) = contexts.first() else { return false }; + let Some(context) = contexts.first() else { + return false; + }; match self { Self::Identifier(name) => (&context.set).contains(name.as_str()), Self::Equal(left, right) => context diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1b83ca59646cbe9f434aabed1c4f12b48778ca66..7569925256f098c2c4c63128213b696c1ae7dc88 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2484,7 +2484,9 @@ impl BufferSnapshot { matches.advance(); - let Some((open, close)) = open.zip(close) else { continue }; + let Some((open, close)) = open.zip(close) else { + continue; + }; let bracket_range = open.start..=close.end; if !bracket_range.overlaps(&range) { diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index b6e1d16e18beac7c9b1282a639a619b57038867e..18f2e9b264159299b92148019ffcfe5de7006dca 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -310,7 +310,9 @@ impl SyntaxSnapshot { // Ignore edits that end before the start of this layer, and don't consider them // for any subsequent layers at this same depth. loop { - let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { continue 'outer }; + let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { + continue 'outer; + }; if edit_range.end.cmp(&layer.range.start, text).is_le() { first_edit_ix_for_depth += 1; } else { @@ -391,7 +393,9 @@ impl SyntaxSnapshot { .filter::<_, ()>(|summary| summary.contains_unknown_injections); cursor.next(text); while let Some(layer) = cursor.item() { - let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() }; + let SyntaxLayerContent::Pending { language_name } = &layer.content else { + unreachable!() + }; if registry .language_for_name_or_extension(language_name) .now_or_never() @@ -533,7 +537,9 @@ impl SyntaxSnapshot { let content = match step.language { ParseStepLanguage::Loaded { language } => { - let Some(grammar) = language.grammar() else { continue }; + let Some(grammar) = language.grammar() else { + continue; + }; let tree; let changed_ranges; diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index c7babf207efcb2fdb30ec19c65adc7589f193ec4..bd50608122b80e9dd3ceba0a20d6b29dbb9f07c4 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -932,8 +932,12 @@ fn check_interpolation( .zip(new_syntax_map.layers.iter()) { assert_eq!(old_layer.range, new_layer.range); - let Some(old_tree) = old_layer.content.tree() else { continue }; - let Some(new_tree) = new_layer.content.tree() else { continue }; + let Some(old_tree) = old_layer.content.tree() else { + continue; + }; + let Some(new_tree) = new_layer.content.tree() else { + continue; + }; let old_start_byte = old_layer.range.start.to_offset(old_buffer); let new_start_byte = new_layer.range.start.to_offset(new_buffer); let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point(); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 16fb019c62a81bfcbf0c37332ef0723347fb5ad8..d18f57ffe95386d3b986e30a8cddad27c4f250fd 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -549,7 +549,9 @@ impl View for LspLogToolbarItemView { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx).clone(); - let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; + let Some(log_view) = self.log_view.as_ref() else { + return Empty::new().into_any(); + }; let log_view = log_view.read(cx); let menu_rows = log_view.menu_items(cx).unwrap_or_default(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 292f9a5226dfb4897c4faa94ab35e6108d4ec136..8ca2571869f4575262cd9453015dec5ef9dfcd7d 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1655,7 +1655,11 @@ impl LspCommand for OnTypeFormatting { type ProtoRequest = proto::OnTypeFormatting; fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { - let Some(on_type_formatting_options) = &server_capabilities.document_on_type_formatting_provider else { return false }; + let Some(on_type_formatting_options) = + &server_capabilities.document_on_type_formatting_provider + else { + return false; + }; on_type_formatting_options .first_trigger_character .contains(&self.trigger) @@ -1769,7 +1773,9 @@ impl LspCommand for OnTypeFormatting { _: ModelHandle, _: AsyncAppContext, ) -> Result> { - let Some(transaction) = message.transaction else { return Ok(None) }; + let Some(transaction) = message.transaction else { + return Ok(None); + }; Ok(Some(language::proto::deserialize_transaction(transaction)?)) } @@ -2238,7 +2244,9 @@ impl LspCommand for InlayHints { type ProtoRequest = proto::InlayHints; fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { - let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false }; + let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { + return false; + }; match inlay_hint_provider { lsp::OneOf::Left(enabled) => *enabled, lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 9e30796bbc58e3176ef73e8acb95358565fc6b34..c128de79107118e421fa306f743716ca38e051f2 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2317,9 +2317,10 @@ impl BackgroundScannerState { for changed_path in changed_paths { let Some(dot_git_dir) = changed_path .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) else { - continue; - }; + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + else { + continue; + }; // Avoid processing the same repository multiple times, if multiple paths // within it have changed. @@ -2348,7 +2349,10 @@ impl BackgroundScannerState { let Some(work_dir) = self .snapshot .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue }; + .map(|entry| RepositoryWorkDirectory(entry.path.clone())) + else { + continue; + }; log::info!("reload git repository {:?}", dot_git_dir); let repository = repository.repo_ptr.lock(); diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 3055399c139ddff1c469d58c4be979943c77b345..1804c2b1fce77054cc21b4d5f119da32531a8bf8 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -40,7 +40,9 @@ impl View for QuickActionBar { } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - let Some(editor) = self.active_editor() else { return Empty::new().into_any(); }; + let Some(editor) = self.active_editor() else { + return Empty::new().into_any(); + }; let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); let mut bar = Flex::row().with_child(render_quick_action_bar_button( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index eba4058729513335d05710511fd573b3b54d3c83..d7633a45e49af1a90a86d6bdce2dba95d209d74b 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -885,7 +885,9 @@ impl ProjectSearchView { if !dir_entry.is_dir() { return; } - let Some(filter_str) = dir_entry.path.to_str() else { return; }; + let Some(filter_str) = dir_entry.path.to_str() else { + return; + }; let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let search = cx.add_view(|cx| ProjectSearchView::new(model, cx)); diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 70495b59d30cc9bb47eb9aa66e6cf22e73d720da..736f2c98a8b6ac92f8b857b961e5e5a796135f65 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -57,7 +57,9 @@ pub fn init( cx.subscribe_global::({ move |event, cx| { - let Some(semantic_index) = SemanticIndex::global(cx) else { return; }; + let Some(semantic_index) = SemanticIndex::global(cx) else { + return; + }; let workspace = &event.0; if let Some(workspace) = workspace.upgrade(cx) { let project = workspace.read(cx).project().clone(); diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 93cb2ab3d74bd873f55c75d4b4415e7fbf782b51..28cc2db784d5d4a9f4cfc8f2049a18171f1ce551 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -63,20 +63,23 @@ impl KeymapFile { // string. But `RawValue` currently does not work inside of an untagged enum. match action { Value::Array(items) => { - let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else { + let Ok([name, data]): Result<[serde_json::Value; 2], _> = + items.try_into() + else { return Some(Err(anyhow!("Expected array of length 2"))); }; let serde_json::Value::String(name) = name else { - return Some(Err(anyhow!("Expected first item in array to be a string."))) + return Some(Err(anyhow!( + "Expected first item in array to be a string." + ))); }; - cx.deserialize_action( - &name, - Some(data), - ) - }, + cx.deserialize_action(&name, Some(data)) + } Value::String(name) => cx.deserialize_action(&name, None), Value::Null => Ok(no_action()), - _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))), + _ => { + return Some(Err(anyhow!("Expected two-element array, got {action:?}"))) + } } .with_context(|| { format!( diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 5d2055051792c8967d2ca7d2d4ffa46940fc5c29..73ed4b059ea37ba6770280a2e84318d1c4517aec 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -107,20 +107,15 @@ impl PickerDelegate for BranchListDelegate { let delegate = view.delegate(); let project = delegate.workspace.read(cx).project().read(&cx); - let Some(worktree) = project - .visible_worktrees(cx) - .next() - else { + let Some(worktree) = project.visible_worktrees(cx).next() else { bail!("Cannot update branch list as there are no visible worktrees") }; - let mut cwd = worktree .read(cx) - .abs_path() - .to_path_buf(); + let mut cwd = worktree.read(cx).abs_path().to_path_buf(); cwd.push(".git"); - let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")}; - let mut branches = repo - .lock() - .branches()?; + let Some(repo) = project.fs().open_repo(&cwd) else { + bail!("Project does not have associated git repository.") + }; + let mut branches = repo.lock().branches()?; const RECENT_BRANCHES_COUNT: usize = 10; if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT { // Truncate list of recent branches @@ -142,8 +137,13 @@ impl PickerDelegate for BranchListDelegate { }) .collect::>()) }) - .log_err() else { return; }; - let Some(candidates) = candidates.log_err() else {return;}; + .log_err() + else { + return; + }; + let Some(candidates) = candidates.log_err() else { + return; + }; let matches = if query.is_empty() { candidates .into_iter() @@ -184,7 +184,11 @@ impl PickerDelegate for BranchListDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { let current_pick = self.selected_index(); - let Some(current_pick) = self.matches.get(current_pick).map(|pick| pick.string.clone()) else { + let Some(current_pick) = self + .matches + .get(current_pick) + .map(|pick| pick.string.clone()) + else { return; }; cx.spawn(|picker, mut cx| async move { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 3d16bb355246688096f425a3ab5076a3864d9e25..3c437f91779ba27f2f2f36c555e6574b2158094b 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -33,7 +33,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { editor.set_clip_at_line_ends(false, cx); let Some(item) = cx.read_from_clipboard() else { - return + return; }; let clipboard_text = Cow::Borrowed(item.text()); if clipboard_text.is_empty() { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index d198eb628d1a2d198bb1b584db7c9a43d2569ec3..6d40f844e70cd3777ab0b8c554cbb06209996381 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -77,7 +77,10 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex } let Some((new_head, goal)) = - motion.move_point(map, current_head, selection.goal, times) else { return }; + motion.move_point(map, current_head, selection.goal, times) + else { + return; + }; selection.set_head(new_head, goal); @@ -132,7 +135,7 @@ pub fn visual_block_motion( } let Some((new_head, _)) = move_selection(&map, head, goal) else { - return + return; }; head = new_head; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 7528fb746864e2c66f013cf88014e855fa52a20a..93fb484214fc181d4636845cf1e00a19760b2fb3 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -742,8 +742,8 @@ mod element { while proposed_current_pixel_change.abs() > 0. { let Some(current_ix) = successors.next() else { - break; - }; + break; + }; let next_target_size = f32::max( size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 62bb7a82a29619d7f6bec11053db56cbf75a5faf..be8148256d0b0f294bbeeafbcfb59e47ec4862d7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2314,8 +2314,12 @@ impl Workspace { item_id_to_move: usize, cx: &mut ViewContext, ) { - let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; }; - let Some(from) = from.upgrade(cx) else { return; }; + let Some(pane_to_split) = pane_to_split.upgrade(cx) else { + return; + }; + let Some(from) = from.upgrade(cx) else { + return; + }; let new_pane = self.add_pane(cx); self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx); diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 41ad28ba862e38e04b52ec5e4e1b77e87b183200..331d829cbc78f24bcc5a641bc11c556de2cd11bc 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -89,7 +89,9 @@ impl LspAdapter for PythonLspAdapter { // to allow our own fuzzy score to be used to break ties. // // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 - let Some(sort_text) = &mut item.sort_text else { return }; + let Some(sort_text) = &mut item.sort_text else { + return; + }; let mut parts = sort_text.split('.'); let Some(first) = parts.next() else { return }; let Some(second) = parts.next() else { return }; From f798be6e273d6744663a6988afc01a3463a4c5bd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 10:25:21 -0700 Subject: [PATCH 086/142] Fix rust 1.72 warnings about shadowed glob re-exports --- crates/language/src/buffer.rs | 2 +- crates/language/src/language.rs | 2 +- crates/lsp/src/lsp.rs | 2 +- crates/project/src/project_tests.rs | 2 +- crates/project/src/worktree.rs | 4 ++-- crates/project/src/worktree_tests.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7569925256f098c2c4c63128213b696c1ae7dc88..d5586af5ef8fd9088685a6934c16ea62169d953f 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -14,7 +14,7 @@ use crate::{ CodeLabel, LanguageScope, Outline, }; use anyhow::{anyhow, Result}; -use clock::ReplicaId; +pub use clock::ReplicaId; use fs::LineEnding; use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 82245d67ca0487adb33aac8ec124f658948c3509..7a9e6b83ceb48584211792239fe2a802ec2e886f 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -18,7 +18,7 @@ use futures::{ FutureExt, TryFutureExt as _, }; use gpui::{executor::Background, AppContext, AsyncAppContext, Task}; -use highlight_map::HighlightMap; +pub use highlight_map::HighlightMap; use lazy_static::lazy_static; use lsp::{CodeActionKind, LanguageServerBinary}; use parking_lot::{Mutex, RwLock}; diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d8069c08b12e11b8b12155892dc974ae0d..7cba03955280dbed1cd98fd9a7b61a1b526c440f 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -77,7 +77,7 @@ pub enum Subscription { } #[derive(Serialize, Deserialize)] -struct Request<'a, T> { +pub struct Request<'a, T> { jsonrpc: &'static str, id: usize, method: &'a str, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 259c10ca057c8bb29ad5b2d805107eb982239441..a504900c83e9178f9ee6cbcf6a66e77a8fa44c0d 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,4 +1,4 @@ -use crate::{search::PathMatcher, worktree::WorktreeHandle, Event, *}; +use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; use fs::{FakeFs, LineEnding, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe, AppContext}; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c128de79107118e421fa306f743716ca38e051f2..e6e0f37cc74b317c5d9de9adda90f9820c230777 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4030,7 +4030,7 @@ struct UpdateIgnoreStatusJob { scan_queue: Sender, } -pub trait WorktreeHandle { +pub trait WorktreeModelHandle { #[cfg(any(test, feature = "test-support"))] fn flush_fs_events<'a>( &self, @@ -4038,7 +4038,7 @@ pub trait WorktreeHandle { ) -> futures::future::LocalBoxFuture<'a, ()>; } -impl WorktreeHandle for ModelHandle { +impl WorktreeModelHandle for ModelHandle { // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that // occurred before the worktree was constructed. These events can cause the worktree to perform // extra directory scans, and emit extra scan-state notifications. diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 6f5b3635096e334b57357f633370782f6a2a965a..4253f45b0ce912412b0f9716474f92d0f875f026 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -1,5 +1,5 @@ use crate::{ - worktree::{Event, Snapshot, WorktreeHandle}, + worktree::{Event, Snapshot, WorktreeModelHandle}, Entry, EntryKind, PathChange, Worktree, }; use anyhow::Result; From 1f3e009b3255d46a7708b2a4f2c1edd1f35a8d05 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 11:34:07 -0600 Subject: [PATCH 087/142] Fix zed-industries/community#1950 --- crates/vim/src/visual.rs | 36 ++++++++++++++----- .../vim/test_data/test_visual_block_mode.json | 6 ++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 6d40f844e70cd3777ab0b8c554cbb06209996381..b68da870f0d579bd555e6adba7b8e0890286c419 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -126,10 +126,15 @@ pub fn visual_block_motion( let map = &s.display_map(); let mut head = s.newest_anchor().head().to_display_point(map); let mut tail = s.oldest_anchor().tail().to_display_point(map); - let mut goal = s.newest_anchor().goal; - let was_reversed = tail.column() > head.column(); + let (start, end) = match s.newest_anchor().goal { + SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), + SelectionGoal::Column(start) if preserve_goal => (start, start + 1), + _ => (tail.column(), head.column()), + }; + let goal = SelectionGoal::ColumnRange { start, end }; + let was_reversed = tail.column() > head.column(); if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } @@ -149,13 +154,6 @@ pub fn visual_block_motion( head = movement::saturating_right(map, head) } - let (start, end) = match goal { - SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), - SelectionGoal::Column(start) if preserve_goal => (start, start + 1), - _ => (tail.column(), head.column()), - }; - goal = SelectionGoal::ColumnRange { start, end }; - let columns = if is_reversed { head.column()..tail.column() } else if head.column() == tail.column() { @@ -791,6 +789,26 @@ mod test { " }) .await; + + //https://github.com/zed-industries/community/issues/1950 + cx.set_shared_state(indoc! { + "Theˇ quick brown + + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"]) + .await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick brown + + fox «jˇ»umps over + the lazy dog + " + }) + .await; } #[gpui::test] diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json index ac306de4ab783715fce34305fd3551a6a3d57131..2239ef43a8037d06d91be33afb45488fbda204fd 100644 --- a/crates/vim/test_data/test_visual_block_mode.json +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -30,3 +30,9 @@ {"Key":"o"} {"Key":"escape"} {"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}} +{"Put":{"state":"Theˇ quick brown\n\nfox jumps over\nthe lazy dog\n"}} +{"Key":"l"} +{"Key":"ctrl-v"} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"The «qˇ»uick brown\n\nfox «jˇ»umps over\nthe lazy dog\n","mode":"VisualBlock"}} From 790aa5d476f2263f2a08a705bb82c7ce5b8d2b53 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 12:53:40 -0600 Subject: [PATCH 088/142] Add relative_line_mode Co-Authored-By: joseph@zed.dev --- assets/settings/default.json | 1 + crates/editor/src/editor_settings.rs | 2 + crates/editor/src/element.rs | 81 +++++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 24412b883bf0be12cb2639dd54dec7f70adf6882..c6c3ff59918d190809ee37ddf71b1fd19fe58e23 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -98,6 +98,7 @@ // Whether to show selections in the scrollbar. "selections": true }, + "relative_line_numbers": false, // Inlay hint related settings "inlay_hints": { // Global switch to toggle hints on and off, switched off by default. diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index f4499b5651cc158c66df3faad7f0ecf707e01bb6..b06f23429a15b17368c8da23a755ad2fe3c637c5 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -9,6 +9,7 @@ pub struct EditorSettings { pub show_completions_on_input: bool, pub use_on_type_format: bool, pub scrollbar: Scrollbar, + pub relative_line_numbers: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -34,6 +35,7 @@ pub struct EditorSettingsContent { pub show_completions_on_input: Option, pub use_on_type_format: Option, pub scrollbar: Option, + pub relative_line_numbers: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3ba807308c6718e2fcbf7e8edf0f9c9f9d08824e..d088fc2e3a1659e65df42d872c659abf162dda58 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1439,10 +1439,47 @@ impl EditorElement { .collect() } + fn calculate_relative_line_numbers( + &self, + rows: &Range, + buffer_rows: &Vec>, + relative_to: Option, + ) -> HashMap { + let mut relative_rows: HashMap = Default::default(); + if let Some(relative_to) = relative_to { + let head_idx = (relative_to - rows.start) as usize; + let mut delta = 1; + let mut i = head_idx + 1; + while i < buffer_rows.len() { + if buffer_rows[i].is_some() { + relative_rows.insert(i, delta); + delta += 1; + } + i += 1; + } + delta = 1; + i = head_idx; + while i > 0 && buffer_rows[i].is_none() { + i -= 1; + } + + while i > 0 { + i -= 1; + if buffer_rows[i].is_some() { + relative_rows.insert(i, delta); + delta += 1; + } + } + } + + relative_rows + } + fn layout_line_numbers( &self, rows: Range, active_rows: &BTreeMap, + newest_selection_head: Option, is_singleton: bool, snapshot: &EditorSnapshot, cx: &ViewContext, @@ -1455,21 +1492,33 @@ impl EditorElement { let mut line_number_layouts = Vec::with_capacity(rows.len()); let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); - for (ix, row) in snapshot + let is_relative = settings::get::(cx).relative_line_numbers; + let relative_to = if is_relative { + newest_selection_head.map(|head| head.row()) + } else { + None + }; + + let buffer_rows = snapshot .buffer_rows(rows.start) .take((rows.end - rows.start) as usize) - .enumerate() - { + .collect::>(); + + let relative_rows = self.calculate_relative_line_numbers(&rows, &buffer_rows, relative_to); + + for (ix, row) in buffer_rows.iter().enumerate() { let display_row = rows.start + ix as u32; let (active, color) = if active_rows.contains_key(&display_row) { (true, style.line_number_active) } else { (false, style.line_number) }; - if let Some(buffer_row) = row { + if let Some(buffer_row) = *row { if include_line_numbers { line_number.clear(); - write!(&mut line_number, "{}", buffer_row + 1).unwrap(); + let default_number = buffer_row + 1; + let number = relative_rows.get(&ix).unwrap_or(&default_number); + write!(&mut line_number, "{}", number).unwrap(); line_number_layouts.push(Some(cx.text_layout_cache().layout_str( &line_number, style.text.font_size, @@ -2296,6 +2345,7 @@ impl Element for EditorElement { let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, + newest_selection_head, is_singleton, &snapshot, cx, @@ -3054,7 +3104,6 @@ mod tests { #[gpui::test] fn test_layout_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx .add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); @@ -3066,10 +3115,28 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); element - .layout_line_numbers(0..6, &Default::default(), false, &snapshot, cx) + .layout_line_numbers(0..6, &Default::default(), None, false, &snapshot, cx) .0 }); assert_eq!(layouts.len(), 6); + + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + let rows = 0..6; + let buffer_rows = snapshot + .buffer_rows(rows.start) + .take((rows.end - rows.start) as usize) + .collect::>(); + + element.calculate_relative_line_numbers(&rows, &buffer_rows, Some(3)) + }); + assert_eq!(relative_rows[&0], 3); + assert_eq!(relative_rows[&1], 2); + assert_eq!(relative_rows[&2], 1); + // current line has no relative number + assert_eq!(relative_rows[&4], 1); + assert_eq!(relative_rows[&5], 2); } #[gpui::test] From 8d5dc266a3b83f1c7b776049b96820f433dbcffa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 23:39:13 -0600 Subject: [PATCH 089/142] Fix relative line numbers when newest cursor offscreen --- crates/editor/src/element.rs | 108 ++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d088fc2e3a1659e65df42d872c659abf162dda58..04f84fb6164238407b7ffc2cde1f955624f6b00c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1441,34 +1441,48 @@ impl EditorElement { fn calculate_relative_line_numbers( &self, + snapshot: &EditorSnapshot, rows: &Range, - buffer_rows: &Vec>, relative_to: Option, - ) -> HashMap { - let mut relative_rows: HashMap = Default::default(); - if let Some(relative_to) = relative_to { - let head_idx = (relative_to - rows.start) as usize; - let mut delta = 1; - let mut i = head_idx + 1; - while i < buffer_rows.len() { - if buffer_rows[i].is_some() { - relative_rows.insert(i, delta); - delta += 1; + ) -> HashMap { + let mut relative_rows: HashMap = Default::default(); + let Some(relative_to) = relative_to else { + return relative_rows; + }; + + let start = rows.start.min(relative_to); + let end = rows.end.max(relative_to); + + let buffer_rows = snapshot + .buffer_rows(start) + .take(1 + (end - start) as usize) + .collect::>(); + + let head_idx = relative_to - start; + let mut delta = 1; + let mut i = head_idx + 1; + while i < buffer_rows.len() as u32 { + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); } - i += 1; - } - delta = 1; - i = head_idx; - while i > 0 && buffer_rows[i].is_none() { - i -= 1; + delta += 1; } + i += 1; + } + delta = 1; + i = head_idx.min(buffer_rows.len() as u32 - 1); + while i > 0 && buffer_rows[i as usize].is_none() { + i -= 1; + } - while i > 0 { - i -= 1; - if buffer_rows[i].is_some() { - relative_rows.insert(i, delta); - delta += 1; + while i > 0 { + i -= 1; + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); } + delta += 1; } } @@ -1499,25 +1513,26 @@ impl EditorElement { None }; - let buffer_rows = snapshot + let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to); + + for (ix, row) in snapshot .buffer_rows(rows.start) .take((rows.end - rows.start) as usize) - .collect::>(); - - let relative_rows = self.calculate_relative_line_numbers(&rows, &buffer_rows, relative_to); - - for (ix, row) in buffer_rows.iter().enumerate() { + .enumerate() + { let display_row = rows.start + ix as u32; let (active, color) = if active_rows.contains_key(&display_row) { (true, style.line_number_active) } else { (false, style.line_number) }; - if let Some(buffer_row) = *row { + if let Some(buffer_row) = row { if include_line_numbers { line_number.clear(); let default_number = buffer_row + 1; - let number = relative_rows.get(&ix).unwrap_or(&default_number); + let number = relative_rows + .get(&(ix as u32 + rows.start)) + .unwrap_or(&default_number); write!(&mut line_number, "{}", number).unwrap(); line_number_layouts.push(Some(cx.text_layout_cache().layout_str( &line_number, @@ -2345,7 +2360,7 @@ impl Element for EditorElement { let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, - newest_selection_head, + newest_selection_head.or_else(|| Some(editor.selections.newest_display(cx).head())), is_singleton, &snapshot, cx, @@ -3122,14 +3137,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - - let rows = 0..6; - let buffer_rows = snapshot - .buffer_rows(rows.start) - .take((rows.end - rows.start) as usize) - .collect::>(); - - element.calculate_relative_line_numbers(&rows, &buffer_rows, Some(3)) + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3137,6 +3145,28 @@ mod tests { // current line has no relative number assert_eq!(relative_rows[&4], 1); assert_eq!(relative_rows[&5], 2); + + // works if cursor is before screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&3], 2); + assert_eq!(relative_rows[&4], 3); + assert_eq!(relative_rows[&5], 4); + + // works if cursor is after screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&0], 5); + assert_eq!(relative_rows[&1], 4); + assert_eq!(relative_rows[&2], 3); } #[gpui::test] From f18cdcba546f0965aa2708324064e4639c46eac5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 10:56:29 -0600 Subject: [PATCH 090/142] Fix relative line numbers in vim visual mode In visual mode when your selection ends with a newline we show the cursor at the end of the previous line (not the start of the current line). We had only been accounting for this if the cursor was on-screen. --- crates/editor/src/element.rs | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 04f84fb6164238407b7ffc2cde1f955624f6b00c..2a623b9b6bcb0a14d7acb423b6407fa1b410c59e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1493,7 +1493,7 @@ impl EditorElement { &self, rows: Range, active_rows: &BTreeMap, - newest_selection_head: Option, + newest_selection_head: DisplayPoint, is_singleton: bool, snapshot: &EditorSnapshot, cx: &ViewContext, @@ -1508,7 +1508,7 @@ impl EditorElement { let mut line_number = String::new(); let is_relative = settings::get::(cx).relative_line_numbers; let relative_to = if is_relative { - newest_selection_head.map(|head| head.row()) + Some(newest_selection_head.row()) } else { None }; @@ -2357,10 +2357,22 @@ impl Element for EditorElement { }) .collect(); + let head_for_relative = newest_selection_head.unwrap_or_else(|| { + let newest = editor.selections.newest::(cx); + SelectionLayout::new( + newest, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + true, + ) + .head + }); + let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, - newest_selection_head.or_else(|| Some(editor.selections.newest_display(cx).head())), + head_for_relative, is_singleton, &snapshot, cx, @@ -3130,14 +3142,21 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); element - .layout_line_numbers(0..6, &Default::default(), None, false, &snapshot, cx) + .layout_line_numbers( + 0..6, + &Default::default(), + DisplayPoint::new(0, 0), + false, + &snapshot, + cx, + ) .0 }); assert_eq!(layouts.len(), 6); let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) + element.calculate_relative_line_numbers(&snapshot, &(0..6), 3) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3150,7 +3169,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + element.calculate_relative_line_numbers(&snapshot, &(3..6), 1) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&3], 2); @@ -3161,7 +3180,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + element.calculate_relative_line_numbers(&snapshot, &(0..3), 6) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&0], 5); From bde67b2b9c6fff9d1977e43efd251bff023a53b0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 11:08:39 -0600 Subject: [PATCH 091/142] Fix merge-conflict --- crates/editor/src/element.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2a623b9b6bcb0a14d7acb423b6407fa1b410c59e..6f93b07f654067ceb5a95eb86173396d647cc7a9 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2365,6 +2365,7 @@ impl Element for EditorElement { editor.cursor_shape, &snapshot.display_snapshot, true, + true, ) .head }); @@ -3156,7 +3157,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..6), 3) + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3169,7 +3170,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(3..6), 1) + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&3], 2); @@ -3180,7 +3181,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..3), 6) + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&0], 5); From 507a5db09c53014e8e45c00fe39c71e992f978ab Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 25 Aug 2023 15:06:31 -0400 Subject: [PATCH 092/142] WIP Co-Authored-By: Mikayla Maki --- assets/settings/default.json | 27 ++- crates/project/src/terminals.rs | 59 +++---- crates/terminal/src/terminal.rs | 122 +------------- crates/terminal/src/terminal_settings.rs | 163 +++++++++++++++++++ crates/terminal_view/src/terminal_element.rs | 3 +- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 5 +- 7 files changed, 213 insertions(+), 168 deletions(-) create mode 100644 crates/terminal/src/terminal_settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 27be6ae5d28204eb29049ea2eaa650e55892fa4c..b1d36c938f819eac4468e7dc816a90eb62b43fa9 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -333,6 +333,24 @@ // "custom": 2 // }, "line_height": "comfortable", + // Activate the python virtual environment, if one is found, in the + // terminal's working directory (as resolved by the working_directory + // setting). Set this to "off" to disable this behavior. + "detect_venv": { + "on": { + // Default directories to search for virtual environments, relative + // to the current working directory. We recommend overriding this + // in your project's settings, rather than globally. + "directories": [ + ".env", + "env", + ".venv", + "venv" + ], + // Can also be 'csh' and 'fish' + "activate_script": "default" + } + } // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. // "font_size": "15", @@ -340,15 +358,6 @@ // the terminal will default to matching the buffer's font family. // "font_family": "Zed Mono", // --- - // Whether or not to automatically search for, and activate, Python virtual - // environments. - // Current limitations: - // - Only ".env", "env", ".venv", and "venv" are searched for at the - // root of the project - // - Only works with Posix-complaint shells - // - Only activates the first virtual environment it finds, regardless - // of the nunber of projects in the workspace. - "activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 2fb66a6c4c95b06acf4a305ec6652d2f2ee3a653..68a043131684619d0a2cb12e2d18f52fd4e3ebaa 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,10 @@ use crate::Project; use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; -use std::path::PathBuf; -use terminal::{Shell, Terminal, TerminalBuilder, TerminalSettings}; +use std::path::{Path, PathBuf}; +use terminal::{ + terminal_settings::{self, TerminalSettings, VenvSettingsContent}, + Terminal, TerminalBuilder, +}; #[cfg(target_os = "macos")] use std::os::unix::ffi::OsStrExt; @@ -23,8 +26,7 @@ impl Project { )); } else { let settings = settings::get::(cx); - let activate_python_virtual_environment = - settings.activate_python_virtual_environment.clone(); + let python_settings = settings.detect_venv.clone(); let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( @@ -53,15 +55,15 @@ impl Project { }) .detach(); - if activate_python_virtual_environment { - let activate_script_path = self.find_activate_script_path(&shell, cx); + if let Some(python_settings) = &python_settings.as_option() { + let activate_script_path = + self.find_activate_script_path(&python_settings, working_directory); self.activate_python_virtual_environment( activate_script_path, &terminal_handle, cx, ); } - terminal_handle }); @@ -71,37 +73,26 @@ impl Project { pub fn find_activate_script_path( &mut self, - shell: &Shell, - cx: &mut ModelContext, + settings: &VenvSettingsContent, + working_directory: Option, ) -> Option { - let program = match shell { - terminal::Shell::System => "Figure this out", - terminal::Shell::Program(program) => program, - terminal::Shell::WithArguments { program, args: _ } => program, + // When we are unable to resolve the working directory, the terminal builder + // defaults to '/'. We should probably encode this directly somewhere, but for + // now, let's just hard code it here. + let working_directory = working_directory.unwrap_or_else(|| Path::new("/").to_path_buf()); + let activate_script_name = match settings.activate_script { + terminal_settings::ActivateScript::Default => "activate", + terminal_settings::ActivateScript::Csh => "activate.csh", + terminal_settings::ActivateScript::Fish => "activate.fish", }; - // This is so hacky - find a better way to do this - let script_name = if program.contains("fish") { - "activate.fish" - } else { - "activate" - }; + for virtual_environment_name in settings.directories { + let mut path = working_directory.join(virtual_environment_name); + path.push("bin/"); + path.push(activate_script_name); - let worktree_paths = self - .worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()); - - const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; - - for worktree_path in worktree_paths { - for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { - let mut path = worktree_path.join(virtual_environment_name); - path.push("bin/"); - path.push(script_name); - - if path.exists() { - return Some(path); - } + if path.exists() { + return Some(path); } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 73ff09225f62c5c09185d074163f9eea631decf1..e28e0ca5c16bb15454aa2d9bbc248963df56c2a9 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,5 +1,6 @@ pub mod mappings; pub use alacritty_terminal; +pub mod terminal_settings; use alacritty_terminal::{ ansi::{ClearMode, Handler}, @@ -31,8 +32,8 @@ use mappings::mouse::{ }; use procinfo::LocalProcessInfo; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings}; use util::truncate_and_trailoff; use std::{ @@ -48,7 +49,6 @@ use std::{ use thiserror::Error; use gpui::{ - fonts, geometry::vector::{vec2f, Vector2F}, keymap_matcher::Keystroke, platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase}, @@ -134,124 +134,6 @@ pub fn init(cx: &mut AppContext) { settings::register::(cx); } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TerminalDockPosition { - Left, - Bottom, - Right, -} - -#[derive(Deserialize)] -pub struct TerminalSettings { - pub shell: Shell, - pub working_directory: WorkingDirectory, - font_size: Option, - pub font_family: Option, - pub line_height: TerminalLineHeight, - pub font_features: Option, - pub env: HashMap, - pub blinking: TerminalBlink, - pub alternate_scroll: AlternateScroll, - pub option_as_meta: bool, - pub copy_on_select: bool, - pub dock: TerminalDockPosition, - pub default_width: f32, - pub default_height: f32, - pub activate_python_virtual_environment: bool, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct TerminalSettingsContent { - pub shell: Option, - pub working_directory: Option, - pub font_size: Option, - pub font_family: Option, - pub line_height: Option, - pub font_features: Option, - pub env: Option>, - pub blinking: Option, - pub alternate_scroll: Option, - pub option_as_meta: Option, - pub copy_on_select: Option, - pub dock: Option, - pub default_width: Option, - pub default_height: Option, - pub activate_python_virtual_environment: Option, -} - -impl TerminalSettings { - pub fn font_size(&self, cx: &AppContext) -> Option { - self.font_size - .map(|size| theme::adjusted_font_size(size, cx)) - } -} - -impl settings::Setting for TerminalSettings { - const KEY: Option<&'static str> = Some("terminal"); - - type FileContent = TerminalSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &AppContext, - ) -> Result { - Self::load_via_json_merge(default_value, user_values) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] -pub enum TerminalLineHeight { - #[default] - Comfortable, - Standard, - Custom(f32), -} - -impl TerminalLineHeight { - pub fn value(&self) -> f32 { - match self { - TerminalLineHeight::Comfortable => 1.618, - TerminalLineHeight::Standard => 1.3, - TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TerminalBlink { - Off, - TerminalControlled, - On, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Shell { - System, - Program(String), - WithArguments { program: String, args: Vec }, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AlternateScroll { - On, - Off, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectory { - CurrentProjectDirectory, - FirstProjectDirectory, - AlwaysHome, - Always { directory: String }, -} - #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub struct TerminalSize { pub cell_width: f32, diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..e0649ebf65cbecb84da761d3a295de08334c6176 --- /dev/null +++ b/crates/terminal/src/terminal_settings.rs @@ -0,0 +1,163 @@ +use std::{collections::HashMap, path::PathBuf}; + +use gpui::{fonts, AppContext}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TerminalDockPosition { + Left, + Bottom, + Right, +} + +#[derive(Deserialize)] +pub struct TerminalSettings { + pub shell: Shell, + pub working_directory: WorkingDirectory, + font_size: Option, + pub font_family: Option, + pub line_height: TerminalLineHeight, + pub font_features: Option, + pub env: HashMap, + pub blinking: TerminalBlink, + pub alternate_scroll: AlternateScroll, + pub option_as_meta: bool, + pub copy_on_select: bool, + pub dock: TerminalDockPosition, + pub default_width: f32, + pub default_height: f32, + pub detect_venv: VenvSettings, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum VenvSettings { + #[default] + Off, + On { + activate_script: Option, + directories: Option>, + }, +} + +pub struct VenvSettingsContent<'a> { + pub activate_script: ActivateScript, + pub directories: &'a [PathBuf], +} + +impl VenvSettings { + pub fn as_option(&self) -> Option { + match self { + VenvSettings::Off => None, + VenvSettings::On { + activate_script, + directories, + } => Some(VenvSettingsContent { + activate_script: activate_script.unwrap_or(ActivateScript::Default), + directories: directories.as_deref().unwrap_or(&[]), + }), + } + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ActivateScript { + #[default] + Default, + Csh, + Fish, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct TerminalSettingsContent { + pub shell: Option, + pub working_directory: Option, + pub font_size: Option, + pub font_family: Option, + pub line_height: Option, + pub font_features: Option, + pub env: Option>, + pub blinking: Option, + pub alternate_scroll: Option, + pub option_as_meta: Option, + pub copy_on_select: Option, + pub dock: Option, + pub default_width: Option, + pub default_height: Option, + pub detect_venv: Option, +} + +impl TerminalSettings { + pub fn font_size(&self, cx: &AppContext) -> Option { + self.font_size + .map(|size| theme::adjusted_font_size(size, cx)) + } +} + +impl settings::Setting for TerminalSettings { + const KEY: Option<&'static str> = Some("terminal"); + + type FileContent = TerminalSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum TerminalLineHeight { + #[default] + Comfortable, + Standard, + Custom(f32), +} + +impl TerminalLineHeight { + pub fn value(&self) -> f32 { + match self { + TerminalLineHeight::Comfortable => 1.618, + TerminalLineHeight::Standard => 1.3, + TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum TerminalBlink { + Off, + TerminalControlled, + On, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Shell { + System, + Program(String), + WithArguments { program: String, args: Vec }, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AlternateScroll { + On, + Off, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectory { + CurrentProjectDirectory, + FirstProjectDirectory, + AlwaysHome, + Always { directory: String }, +} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1d12b83c5c31162d39125322eef282a9d45b7f59..b3d87f531ad5794b86f4b56dbe307e3078a5ffd3 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -25,7 +25,8 @@ use terminal::{ term::{cell::Flags, TermMode}, }, mappings::colors::convert_color, - IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize, + terminal_settings::TerminalSettings, + IndexedCell, Terminal, TerminalContent, TerminalSize, }; use theme::{TerminalStyle, ThemeSettings}; use util::ResultExt; diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 472e748359ea7399a5bcf680c57ffa5a17ad1e8d..9fb3939e1f17a9adfe842130c43684ee4b2cddac 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -9,7 +9,7 @@ use gpui::{ use project::Fs; use serde::{Deserialize, Serialize}; use settings::SettingsStore; -use terminal::{TerminalDockPosition, TerminalSettings}; +use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 92465d6b32cff774480ca33d9adc8b5665a49141..104d181a7b9de60460bafa0f12abac51f4ac22e2 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -33,7 +33,8 @@ use terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, - Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory, + terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, + Event, MaybeNavigationTarget, Terminal, }; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ @@ -44,8 +45,6 @@ use workspace::{ NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; -pub use terminal::TerminalSettings; - const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); ///Event to transmit the scroll from the element to the view From 0280d5d01092d03c854267c42c0c64d45ff1a586 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 12:53:20 -0600 Subject: [PATCH 093/142] vim change for wrapped lines --- crates/vim/src/motion.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 8cd29e5e9f16ffb982e3a76a37038acb3405222b..a85c6fc0a3494ce15cf4c39b1d9a486afdb25566 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{cmp, sync::Arc}; use editor::{ char_kind, @@ -404,9 +404,13 @@ fn down( mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { + let start_row = point.to_point(map).row; + let target = cmp::min(map.max_buffer_row(), start_row + times as u32); + + while point.to_point(map).row < target { (point, goal) = movement::down(map, point, goal, true); } + (point, goal) } @@ -416,7 +420,10 @@ fn up( mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { + let start_row = point.to_point(map).row; + let target = start_row.saturating_sub(times as u32); + + while point.to_point(map).row > target { (point, goal) = movement::up(map, point, goal, true); } (point, goal) From 20aa2a4c54df6268c2c18b36af4c36e599583138 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 22:11:51 -0600 Subject: [PATCH 094/142] vim: Fix relative line motion Before this change up and down were in display co-ordinates, after this change they are in fold coordinates (which matches the vim behaviour). To make this work without causing usabliity problems, a bunch of extra keyboard shortcuts now work: - vim: `z {o,c}` to open,close a fold - vim: `z f` to fold current visual selection - vim: `g {j,k,up,down}` to move up/down a display line - vim: `g {0,^,$,home,end}` to get to start/end of a display line Fixes: zed-industries/community#1562 --- assets/keymaps/vim.json | 57 ++++ crates/editor/src/display_map.rs | 17 +- crates/editor/src/editor.rs | 14 +- crates/vim/src/motion.rs | 265 ++++++++++++++---- crates/vim/src/normal.rs | 27 +- crates/vim/src/normal/change.rs | 6 +- crates/vim/src/normal/substitute.rs | 5 +- crates/vim/src/test.rs | 142 ++++++++++ .../src/test/neovim_backed_test_context.rs | 28 +- crates/vim/src/test/neovim_connection.rs | 24 ++ crates/vim/src/visual.rs | 11 +- crates/vim/test_data/test_folds.json | 23 ++ crates/vim/test_data/test_wrapped_lines.json | 26 ++ 13 files changed, 579 insertions(+), 66 deletions(-) create mode 100644 crates/vim/test_data/test_folds.json create mode 100644 crates/vim/test_data/test_wrapped_lines.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c0de3420f222566220d5f7e487554b3ee5bc33d5..c7e6199f444f6ce09cc1e1d6b39703b91cbd8fd2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -137,10 +137,67 @@ "partialWord": true } ], + "g j": [ + "vim::Down", + { + "displayLines": true + } + ], + "g down": [ + "vim::Down", + { + "displayLines": true + } + ], + "g k": [ + "vim::Up", + { + "displayLines": true + } + ], + "g up": [ + "vim::Up", + { + "displayLines": true + } + ], + "g $": [ + "vim::EndOfLine", + { + "displayLines": true + } + ], + "g end": [ + "vim::EndOfLine", + { + "displayLines": true + } + ], + "g 0": [ + "vim::StartOfLine", + { + "displayLines": true + } + ], + "g home": [ + "vim::StartOfLine", + { + "displayLines": true + } + ], + "g ^": [ + "vim::FirstNonWhitespace", + { + "displayLines": true + } + ], // z commands "z t": "editor::ScrollCursorTop", "z z": "editor::ScrollCursorCenter", "z b": "editor::ScrollCursorBottom", + "z c": "editor::Fold", + "z o": "editor::UnfoldLines", + "z f": "editor::FoldSelectedRanges", // Count support "1": [ "vim::Number", diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 611866bcadeaef851ba081434fadc04a2d3031ae..fae4109b94c7eee9e9883fdf055c168be8e7d035 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -30,6 +30,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; +pub use self::fold_map::FoldPoint; pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -310,7 +311,7 @@ impl DisplayMap { pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, - fold_snapshot: fold_map::FoldSnapshot, + pub fold_snapshot: fold_map::FoldSnapshot, inlay_snapshot: inlay_map::InlaySnapshot, tab_snapshot: tab_map::TabSnapshot, wrap_snapshot: wrap_map::WrapSnapshot, @@ -438,6 +439,20 @@ impl DisplaySnapshot { fold_point.to_inlay_point(&self.fold_snapshot) } + pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint { + let block_point = point.0; + let wrap_point = self.block_snapshot.to_wrap_point(block_point); + let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); + self.tab_snapshot.to_fold_point(tab_point, bias).0 + } + + pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint { + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) + } + pub fn max_point(&self) -> DisplayPoint { DisplayPoint(self.block_snapshot.max_point()) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c5ff1f027da7faac89bbcbf596d501fffbeae2cb..ac9a972f96e8c5f342ebcdb4635b345442215da1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7198,7 +7198,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); + let selections = self.selections.all_adjusted(cx); for selection in selections { let range = selection.range().sorted(); let buffer_start_row = range.start.row; @@ -7274,7 +7274,17 @@ impl Editor { pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { let selections = self.selections.all::(cx); - let ranges = selections.into_iter().map(|s| s.start..s.end); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let line_mode = self.selections.line_mode; + let ranges = selections.into_iter().map(|s| { + if line_mode { + let start = Point::new(s.start.row, 0); + let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row)); + start..end + } else { + s.start..s.end + } + }); self.fold_ranges(ranges, true, cx); } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a85c6fc0a3494ce15cf4c39b1d9a486afdb25566..2edbb8ff1c3264b50db9d295cb717671a9892281 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2,7 +2,7 @@ use std::{cmp, sync::Arc}; use editor::{ char_kind, - display_map::{DisplaySnapshot, ToDisplayPoint}, + display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, movement, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; @@ -21,16 +21,16 @@ use crate::{ pub enum Motion { Left, Backspace, - Down, - Up, + Down { display_lines: bool }, + Up { display_lines: bool }, Right, NextWordStart { ignore_punctuation: bool }, NextWordEnd { ignore_punctuation: bool }, PreviousWordStart { ignore_punctuation: bool }, - FirstNonWhitespace, + FirstNonWhitespace { display_lines: bool }, CurrentLine, - StartOfLine, - EndOfLine, + StartOfLine { display_lines: bool }, + EndOfLine { display_lines: bool }, StartOfParagraph, EndOfParagraph, StartOfDocument, @@ -62,6 +62,41 @@ struct PreviousWordStart { ignore_punctuation: bool, } +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Up { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Down { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct FirstNonWhitespace { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct EndOfLine { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct StartOfLine { + #[serde(default)] + display_lines: bool, +} + #[derive(Clone, Deserialize, PartialEq)] struct RepeatFind { #[serde(default)] @@ -73,12 +108,7 @@ actions!( [ Left, Backspace, - Down, - Up, Right, - FirstNonWhitespace, - StartOfLine, - EndOfLine, CurrentLine, StartOfParagraph, EndOfParagraph, @@ -90,20 +120,63 @@ actions!( ); impl_actions!( vim, - [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind] + [ + NextWordStart, + NextWordEnd, + PreviousWordStart, + RepeatFind, + Up, + Down, + FirstNonWhitespace, + EndOfLine, + StartOfLine, + ] ); pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx)); - cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx)); - cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx)); + cx.add_action(|_: &mut Workspace, action: &Down, cx: _| { + motion( + Motion::Down { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &Up, cx: _| { + motion( + Motion::Up { + display_lines: action.display_lines, + }, + cx, + ) + }); cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); - cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| { - motion(Motion::FirstNonWhitespace, cx) + cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| { + motion( + Motion::FirstNonWhitespace { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| { + motion( + Motion::StartOfLine { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| { + motion( + Motion::EndOfLine { + display_lines: action.display_lines, + }, + cx, + ) }); - cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx)); - cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx)); cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| { motion(Motion::StartOfParagraph, cx) @@ -192,19 +265,25 @@ impl Motion { pub fn linewise(&self) -> bool { use Motion::*; match self { - Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart - | StartOfParagraph | EndOfParagraph => true, - EndOfLine + Down { .. } + | Up { .. } + | StartOfDocument + | EndOfDocument + | CurrentLine + | NextLineStart + | StartOfParagraph + | EndOfParagraph => true, + EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } | Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } => false, } } @@ -213,21 +292,21 @@ impl Motion { use Motion::*; match self { StartOfDocument | EndOfDocument | CurrentLine => true, - Down - | Up - | EndOfLine + Down { .. } + | Up { .. } + | EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } | Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | StartOfParagraph | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } | NextLineStart => false, } @@ -236,12 +315,12 @@ impl Motion { pub fn inclusive(&self) -> bool { use Motion::*; match self { - Down - | Up + Down { .. } + | Up { .. } | StartOfDocument | EndOfDocument | CurrentLine - | EndOfLine + | EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } @@ -249,12 +328,12 @@ impl Motion { Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | StartOfParagraph | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } => false, } } @@ -272,8 +351,18 @@ impl Motion { let (new_point, goal) = match self { Left => (left(map, point, times), SelectionGoal::None), Backspace => (backspace(map, point, times), SelectionGoal::None), - Down => down(map, point, goal, times), - Up => up(map, point, goal, times), + Down { + display_lines: false, + } => down(map, point, goal, times), + Down { + display_lines: true, + } => down_display(map, point, goal, times), + Up { + display_lines: false, + } => up(map, point, goal, times), + Up { + display_lines: true, + } => up_display(map, point, goal, times), Right => (right(map, point, times), SelectionGoal::None), NextWordStart { ignore_punctuation } => ( next_word_start(map, point, *ignore_punctuation, times), @@ -287,9 +376,17 @@ impl Motion { previous_word_start(map, point, *ignore_punctuation, times), SelectionGoal::None, ), - FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), - StartOfLine => (start_of_line(map, point), SelectionGoal::None), - EndOfLine => (end_of_line(map, point), SelectionGoal::None), + FirstNonWhitespace { display_lines } => ( + first_non_whitespace(map, *display_lines, point), + SelectionGoal::None, + ), + StartOfLine { display_lines } => ( + start_of_line(map, *display_lines, point), + SelectionGoal::None, + ), + EndOfLine { display_lines } => { + (end_of_line(map, *display_lines, point), SelectionGoal::None) + } StartOfParagraph => ( movement::start_of_paragraph(map, point, times), SelectionGoal::None, @@ -298,7 +395,7 @@ impl Motion { map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), SelectionGoal::None, ), - CurrentLine => (end_of_line(map, point), SelectionGoal::None), + CurrentLine => (end_of_line(map, false, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => ( end_of_document(map, point, maybe_times), @@ -400,14 +497,38 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di fn down( map: &DisplaySnapshot, - mut point: DisplayPoint, + point: DisplayPoint, mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - let start_row = point.to_point(map).row; - let target = cmp::min(map.max_buffer_row(), start_row + times as u32); + let start = map.display_point_to_fold_point(point, Bias::Left); + + let goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => { + goal = SelectionGoal::Column(start.column()); + start.column() + } + }; - while point.to_point(map).row < target { + let new_row = cmp::min( + start.row() + times as u32, + map.buffer_snapshot.max_point().row, + ); + let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); + let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col)); + + (map.clip_point(point, Bias::Left), goal) +} + +fn down_display( + map: &DisplaySnapshot, + mut point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + for _ in 0..times { (point, goal) = movement::down(map, point, goal, true); } @@ -416,16 +537,38 @@ fn down( fn up( map: &DisplaySnapshot, - mut point: DisplayPoint, + point: DisplayPoint, mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - let start_row = point.to_point(map).row; - let target = start_row.saturating_sub(times as u32); + let start = map.display_point_to_fold_point(point, Bias::Left); + + let goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => { + goal = SelectionGoal::Column(start.column()); + start.column() + } + }; + + let new_row = start.row().saturating_sub(times as u32); + let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); + let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col)); - while point.to_point(map).row > target { + (map.clip_point(point, Bias::Left), goal) +} + +fn up_display( + map: &DisplaySnapshot, + mut point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + for _ in 0..times { (point, goal) = movement::up(map, point, goal, true); } + (point, goal) } @@ -516,8 +659,12 @@ fn previous_word_start( point } -fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { - let mut last_point = DisplayPoint::new(from.row(), 0); +fn first_non_whitespace( + map: &DisplaySnapshot, + display_lines: bool, + from: DisplayPoint, +) -> DisplayPoint { + let mut last_point = start_of_line(map, display_lines, from); let language = map.buffer_snapshot.language_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { @@ -534,12 +681,23 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi map.clip_point(last_point, Bias::Left) } -fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - map.prev_line_boundary(point.to_point(map)).1 +fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { + if display_lines { + map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) + } else { + map.prev_line_boundary(point.to_point(map)).1 + } } -fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) +fn end_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { + if display_lines { + map.clip_point( + DisplayPoint::new(point.row(), map.line_len(point.row())), + Bias::Left, + ) + } else { + map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) + } } fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint { @@ -664,6 +822,7 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> let new_row = (point.row() + times as u32).min(map.max_buffer_row()); first_non_whitespace( map, + false, map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), ) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 3a2d15a878865418f260c5a14dc9b09e5734944b..2b03632c42a2f1a3247c60409d1df2030913d282 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -78,13 +78,27 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); - change_motion(vim, Motion::EndOfLine, times, cx); + change_motion( + vim, + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }) }); cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); - delete_motion(vim, Motion::EndOfLine, times, cx); + delete_motion( + vim, + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }) }); scroll::init(cx); @@ -165,7 +179,10 @@ fn insert_first_non_whitespace( vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::FirstNonWhitespace.move_point(map, cursor, goal, None) + Motion::FirstNonWhitespace { + display_lines: false, + } + .move_point(map, cursor, goal, None) }); }); }); @@ -178,7 +195,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::EndOfLine.move_point(map, cursor, goal, None) + Motion::CurrentLine.move_point(map, cursor, goal, None) }); }); }); @@ -238,7 +255,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::EndOfLine.move_point(map, cursor, goal, None) + Motion::CurrentLine.move_point(map, cursor, goal, None) }); }); editor.edit_with_autoindent(edits, cx); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 50bc049a3aa96d37ae9acce6a1505369333bf534..5591de89c668be823b10f47bf41d2710619ae42c 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -10,7 +10,11 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m // Some motions ignore failure when switching to normal mode let mut motion_succeeded = matches!( motion, - Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine + Motion::Left + | Motion::Right + | Motion::EndOfLine { .. } + | Motion::Backspace + | Motion::StartOfLine { .. } ); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 1d53c6831cc0e92be9b021e1faa114928e03276b..b04596240a25d224e9785b4d169bee9743affe19 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -15,7 +15,10 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { } if line_mode { Motion::CurrentLine.expand_selection(map, selection, None, false); - if let Some((point, _)) = Motion::FirstNonWhitespace.move_point( + if let Some((point, _)) = (Motion::FirstNonWhitespace { + display_lines: false, + }) + .move_point( map, selection.start, selection.goal, diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 9cd927601f518315c133f3830afb712fca00233e..cfde221dc56f71aeb20e7830996dbd580dbf3d28 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -285,3 +285,145 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) { Mode::Visual, ) } + +#[gpui::test] +async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_wrap(12).await; + // tests line wrap as follows: + // 1: twelve char + // twelve char + // 2: twelve char + cx.set_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["j"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + tˇwelve char + "}) + .await; + cx.simulate_shared_keystrokes(["k"]).await; + cx.assert_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["g", "j"]).await; + cx.assert_shared_state(indoc! { " + twelve char tˇwelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["g", "j"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + tˇwelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "k"]).await; + cx.assert_shared_state(indoc! { " + twelve char tˇwelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "^"]).await; + cx.assert_shared_state(indoc! { " + twelve char ˇtwelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["^"]).await; + cx.assert_shared_state(indoc! { " + ˇtwelve char twelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "$"]).await; + cx.assert_shared_state(indoc! { " + twelve charˇ twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["$"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve chaˇr + twelve char + "}) + .await; +} + +#[gpui::test] +async fn test_folds(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_neovim_option("foldmethod=manual").await; + + cx.set_shared_state(indoc! { " + fn boop() { + ˇbarp() + bazp() + } + "}) + .await; + cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"]) + .await; + + // visual display is now: + // fn boop () { + // [FOLDED] + // } + + // TODO: this should not be needed but currently zf does not + // return to normal mode. + cx.simulate_shared_keystrokes(["escape"]).await; + + // skip over fold downward + cx.simulate_shared_keystrokes(["g", "g"]).await; + cx.assert_shared_state(indoc! { " + ˇfn boop() { + barp() + bazp() + } + "}) + .await; + + cx.simulate_shared_keystrokes(["j", "j"]).await; + cx.assert_shared_state(indoc! { " + fn boop() { + barp() + bazp() + ˇ} + "}) + .await; + + // skip over fold upward + cx.simulate_shared_keystrokes(["2", "k"]).await; + cx.assert_shared_state(indoc! { " + ˇfn boop() { + barp() + bazp() + } + "}) + .await; + + // yank the fold + cx.simulate_shared_keystrokes(["down", "y", "y"]).await; + cx.assert_shared_clipboard(" barp()\n bazp()\n").await; + + // re-open + cx.simulate_shared_keystrokes(["z", "o"]).await; + cx.assert_shared_state(indoc! { " + fn boop() { + ˇ barp() + bazp() + } + "}) + .await; +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index f4b0e961839d087b36acd52bd52ba28e54119af5..bc37f2fdd631d0dbf3405890d3f72f455c9aaafd 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,9 +1,14 @@ +use editor::EditorSettings; use indoc::indoc; +use settings::SettingsStore; use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; -use language::OffsetRangeExt; +use language::{ + language_settings::{AllLanguageSettings, LanguageSettings, SoftWrap}, + OffsetRangeExt, +}; use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; @@ -127,6 +132,27 @@ impl<'a> NeovimBackedTestContext<'a> { context_handle } + pub async fn set_shared_wrap(&mut self, columns: u32) { + if columns < 12 { + panic!("nvim doesn't support columns < 12") + } + self.neovim.set_option("wrap").await; + self.neovim.set_option("columns=12").await; + + self.update(|cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength); + settings.defaults.preferred_line_length = Some(columns); + }); + }) + }) + } + + pub async fn set_neovim_option(&mut self, option: &str) { + self.neovim.set_option(option).await; + } + pub async fn assert_shared_state(&mut self, marked_text: &str) { let neovim = self.neovim_state().await; let editor = self.editor_state(); diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 68f3374772bd34bea247a7e3e814c2b83b394c7f..3e59080b13040c81c362528afda42f5e3fa94ff6 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -41,6 +41,7 @@ pub enum NeovimData { Key(String), Get { state: String, mode: Option }, ReadRegister { name: char, value: String }, + SetOption { value: String }, } pub struct NeovimConnection { @@ -222,6 +223,29 @@ impl NeovimConnection { ); } + #[cfg(feature = "neovim")] + pub async fn set_option(&mut self, value: &str) { + self.nvim + .command_output(format!("set {}", value).as_str()) + .await + .unwrap(); + + self.data.push_back(NeovimData::SetOption { + value: value.to_string(), + }) + } + + #[cfg(not(feature = "neovim"))] + pub async fn set_option(&mut self, value: &str) { + assert_eq!( + self.data.pop_front(), + Some(NeovimData::SetOption { + value: value.to_string(), + }), + "operation does not match recorded script. re-record with --features=neovim" + ); + } + #[cfg(not(feature = "neovim"))] pub async fn read_register(&mut self, register: char) -> String { if let Some(NeovimData::Get { .. }) = self.data.front() { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index b68da870f0d579bd555e6adba7b8e0890286c419..ee46a0d209f348f3fc25c2252ca9a8a06921fd68 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -51,8 +51,15 @@ pub fn init(cx: &mut AppContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { - let is_up_or_down = matches!(motion, Motion::Up | Motion::Down); + if vim.state().mode == Mode::VisualBlock + && !matches!( + motion, + Motion::EndOfLine { + display_lines: false + } + ) + { + let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { motion.move_point(map, point, goal, times) }) diff --git a/crates/vim/test_data/test_folds.json b/crates/vim/test_data/test_folds.json new file mode 100644 index 0000000000000000000000000000000000000000..668df5ce269307a6c8d0b89a1ee67019b6746917 --- /dev/null +++ b/crates/vim/test_data/test_folds.json @@ -0,0 +1,23 @@ +{"SetOption":{"value":"foldmethod=manual"}} +{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} +{"Key":"shift-v"} +{"Key":"j"} +{"Key":"z"} +{"Key":"f"} +{"Key":"escape"} +{"Key":"g"} +{"Key":"g"} +{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"fn boop() {\n barp()\n bazp()\nˇ}\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"k"} +{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} +{"Key":"down"} +{"Key":"y"} +{"Key":"y"} +{"ReadRegister":{"name":"\"","value":" barp()\n bazp()\n"}} +{"Key":"z"} +{"Key":"o"} +{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json new file mode 100644 index 0000000000000000000000000000000000000000..f9f54c5c43bf64397a958271cc4d44cbf84e4a9a --- /dev/null +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -0,0 +1,26 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} +{"Key":"j"} +{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} +{"Key":"k"} +{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"j"} +{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"j"} +{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"k"} +{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"^"} +{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"^"} +{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"$"} +{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"$"} +{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} From dee1a433dd7be8dbc0fc5590882ddc4017b7a281 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 22:48:58 -0600 Subject: [PATCH 095/142] A few more fixes for wrapped line motions --- crates/vim/src/motion.rs | 22 +++--- crates/vim/src/normal.rs | 23 +++--- crates/vim/src/test.rs | 73 +++++++++++++++++++ .../src/test/neovim_backed_test_context.rs | 3 +- crates/vim/test_data/test_wrapped_lines.json | 24 ++++++ 5 files changed, 123 insertions(+), 22 deletions(-) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 2edbb8ff1c3264b50db9d295cb717671a9892281..0d3fb700efff6e651ff23769070a37e9cc4d9b20 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -535,7 +535,7 @@ fn down_display( (point, goal) } -fn up( +pub(crate) fn up( map: &DisplaySnapshot, point: DisplayPoint, mut goal: SelectionGoal, @@ -681,7 +681,11 @@ fn first_non_whitespace( map.clip_point(last_point, Bias::Left) } -fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { +pub(crate) fn start_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, +) -> DisplayPoint { if display_lines { map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) } else { @@ -689,7 +693,11 @@ fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint } } -fn end_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { +pub(crate) fn end_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, +) -> DisplayPoint { if display_lines { map.clip_point( DisplayPoint::new(point.row(), map.line_len(point.row())), @@ -819,12 +827,8 @@ fn find_backward( } fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let new_row = (point.row() + times as u32).min(map.max_buffer_row()); - first_non_whitespace( - map, - false, - map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), - ) + let correct_line = down(map, point, SelectionGoal::None, times).0; + first_non_whitespace(map, false, correct_line) } #[cfg(test)] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2b03632c42a2f1a3247c60409d1df2030913d282..a73c51880964c09e41698bbad242c5877aa16796 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -10,7 +10,7 @@ mod yank; use std::sync::Arc; use crate::{ - motion::Motion, + motion::{self, Motion}, object::Object, state::{Mode, Operator}, Vim, @@ -214,19 +214,19 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex .collect(); let edits = selection_start_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); - let start_of_line = map - .clip_point(DisplayPoint::new(row, 0), Bias::Left) - .to_point(&map); + let start_of_line = + motion::start_of_line(&map, false, DisplayPoint::new(row, 0)) + .to_point(&map); let mut new_text = " ".repeat(indent as usize); new_text.push('\n'); (start_of_line..start_of_line, new_text) }); editor.edit_with_autoindent(edits, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.row_mut() -= 1; - *cursor.column_mut() = map.line_len(cursor.row()); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + s.move_cursors_with(|map, cursor, _| { + let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0; + let insert_point = motion::end_of_line(map, false, previous_line); + (insert_point, SelectionGoal::None) }); }); }); @@ -240,15 +240,16 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { let (map, old_selections) = editor.selections.all_display(cx); + let selection_end_rows: HashSet = old_selections .into_iter() .map(|selection| selection.end.row()) .collect(); let edits = selection_end_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); - let end_of_line = map - .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left) - .to_point(&map); + let end_of_line = + motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map); + let mut new_text = "\n".to_string(); new_text.push_str(&" ".repeat(indent as usize)); (end_of_line..end_of_line, new_text) diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index cfde221dc56f71aeb20e7830996dbd580dbf3d28..88fa37585150d3e0749468094feab4bd98545f15 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -358,6 +358,79 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { twelve char "}) .await; + + cx.set_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["enter"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + ˇtwelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["o", "o", "escape"]).await; + cx.assert_shared_state(indoc! { " + twelve char + twelve char twelve char + ˇo + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-a", "a", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + twelve char twelve charˇa + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-i", "i", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + ˇitwelve char twelve chara + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-d"]).await; + cx.assert_shared_state(indoc! { " + twelve char + ˇ + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + twelve char tˇwelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-o", "o", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + ˇo + twelve char twelve char + twelve char + "}) + .await; } #[gpui::test] diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index bc37f2fdd631d0dbf3405890d3f72f455c9aaafd..d04b1b776836b04addddf5e767023393dd5782f7 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,4 +1,3 @@ -use editor::EditorSettings; use indoc::indoc; use settings::SettingsStore; use std::ops::{Deref, DerefMut, Range}; @@ -6,7 +5,7 @@ use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; use language::{ - language_settings::{AllLanguageSettings, LanguageSettings, SoftWrap}, + language_settings::{AllLanguageSettings, SoftWrap}, OffsetRangeExt, }; use util::test::{generate_marked_text, marked_text_offsets}; diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json index f9f54c5c43bf64397a958271cc4d44cbf84e4a9a..1ebbd4f20507a86747a50a39d9ee70cd2266f74b 100644 --- a/crates/vim/test_data/test_wrapped_lines.json +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -24,3 +24,27 @@ {"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} {"Key":"$"} {"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} +{"Key":"enter"} +{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} +{"Key":"o"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} +{"Key":"shift-a"} +{"Key":"a"} +{"Key":"escape"} +{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}} +{"Key":"shift-i"} +{"Key":"i"} +{"Key":"escape"} +{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}} +{"Key":"shift-d"} +{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}} +{"Key":"shift-o"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}} From 6fdf101745d18c16d54423d8b6131d4c8b31679c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 25 Aug 2023 14:34:19 -0700 Subject: [PATCH 096/142] Update database and RPC to provide configured feature flags --- crates/client/src/test.rs | 1 + .../20221109000000_test_schema.sql | 19 ++++++ ...0230825190322_add_server_feature_flags.sql | 16 +++++ crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries/users.rs | 54 +++++++++++++++++ crates/collab/src/db/tables.rs | 2 + crates/collab/src/db/tables/feature_flag.rs | 40 +++++++++++++ crates/collab/src/db/tables/user.rs | 23 +++++++ crates/collab/src/db/tables/user_feature.rs | 42 +++++++++++++ crates/collab/src/db/tests.rs | 1 + .../collab/src/db/tests/feature_flag_tests.rs | 60 +++++++++++++++++++ crates/collab/src/rpc.rs | 15 +++-- crates/rpc/proto/zed.proto | 1 + crates/rpc/src/rpc.rs | 2 +- 14 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 crates/collab/migrations/20230825190322_add_server_feature_flags.sql create mode 100644 crates/collab/src/db/tables/feature_flag.rs create mode 100644 crates/collab/src/db/tables/user_feature.rs create mode 100644 crates/collab/src/db/tests/feature_flag_tests.rs diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 4c12a205660f7932a6a7b412c6ee686a6199372c..00e7cd1508613c60a05ddbba8cabff86bbaf1d14 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -168,6 +168,7 @@ impl FakeServer { GetPrivateUserInfoResponse { metrics_id: "the-metrics-id".into(), staff: false, + flags: Default::default(), }, ) .await; diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7a4cd9fd23cbc80bb38e3b2e7446ae53a902066a..80477dcb3c3b9f4fc1efd25622243b59901cf4fc 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -249,3 +249,22 @@ CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replic CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); + + +CREATE TABLE "feature_flags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "flag" TEXT NOT NULL UNIQUE +); + +CREATE INDEX "index_feature_flags" ON "feature_flags" ("id"); + + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/migrations/20230825190322_add_server_feature_flags.sql b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql new file mode 100644 index 0000000000000000000000000000000000000000..fffde54a20e4869ccbef2093de4e7fe5044132e2 --- /dev/null +++ b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql @@ -0,0 +1,16 @@ +CREATE TABLE "feature_flags" ( + "id" SERIAL PRIMARY KEY, + "flag" VARCHAR(255) NOT NULL UNIQUE +); + +CREATE UNIQUE INDEX "index_feature_flags" ON "feature_flags" ("id"); + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 8501083f839940ed9723813b9aac8a029d706a0d..b33ea57183b8771792ea50c6b3ab2b2631971194 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -125,3 +125,4 @@ id_type!(ServerId); id_type!(SignupId); id_type!(UserId); id_type!(ChannelBufferCollaboratorId); +id_type!(FlagId); diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index bac0f14f8324126fe4f403887aa4eb65e4241de2..bd7c3e9ffd62dea8b0d283fb1c6e1c26e8958d2b 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -240,4 +240,58 @@ impl Database { result.push('%'); result } + + #[cfg(debug_assertions)] + pub async fn create_user_flag(&self, flag: &str) -> Result { + self.transaction(|tx| async move { + let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { + flag: ActiveValue::set(flag.to_string()), + ..Default::default() + }) + .exec(&*tx) + .await? + .last_insert_id; + + Ok(flag) + }) + .await + } + + #[cfg(debug_assertions)] + pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { + self.transaction(|tx| async move { + user_feature::Entity::insert(user_feature::ActiveModel { + user_id: ActiveValue::set(user), + feature_id: ActiveValue::set(flag), + }) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_user_flags(&self, user: UserId) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + Flag, + } + + let flags = user::Model { + id: user, + ..Default::default() + } + .find_linked(user::UserFlags) + .select_only() + .column(feature_flag::Column::Flag) + .into_values::<_, QueryAs>() + .all(&*tx) + .await?; + + Ok(flags) + }) + .await + } } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index fe747e0d27ec1cc5b67b0bbdb55a1c5992fa27b4..1765cee065fb6c7ae31818568a229e3c3c0bd3f0 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -7,6 +7,7 @@ pub mod channel_buffer_collaborator; pub mod channel_member; pub mod channel_path; pub mod contact; +pub mod feature_flag; pub mod follower; pub mod language_server; pub mod project; @@ -16,6 +17,7 @@ pub mod room_participant; pub mod server; pub mod signup; pub mod user; +pub mod user_feature; pub mod worktree; pub mod worktree_diagnostic_summary; pub mod worktree_entry; diff --git a/crates/collab/src/db/tables/feature_flag.rs b/crates/collab/src/db/tables/feature_flag.rs new file mode 100644 index 0000000000000000000000000000000000000000..41c1451c648e7115165a2cf3bfc4e84d9ae534a1 --- /dev/null +++ b/crates/collab/src/db/tables/feature_flag.rs @@ -0,0 +1,40 @@ +use sea_orm::entity::prelude::*; + +use crate::db::FlagId; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "feature_flags")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: FlagId, + pub flag: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeature, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeature.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +pub struct FlaggedUsers; + +impl Linked for FlaggedUsers { + type FromEntity = Entity; + + type ToEntity = super::user::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::Flag.def().rev(), + super::user_feature::Relation::User.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 402b06c2a71a164c9f9314ec0d9e4aa5519156c7..739693527f00a594f3376a6093dc8c0b1d270a8f 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -28,6 +28,8 @@ pub enum Relation { HostedProjects, #[sea_orm(has_many = "super::channel_member::Entity")] ChannelMemberships, + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeatures, } impl Related for Entity { @@ -54,4 +56,25 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeatures.def() + } +} + impl ActiveModelBehavior for ActiveModel {} + +pub struct UserFlags; + +impl Linked for UserFlags { + type FromEntity = Entity; + + type ToEntity = super::feature_flag::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::User.def().rev(), + super::user_feature::Relation::Flag.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user_feature.rs b/crates/collab/src/db/tables/user_feature.rs new file mode 100644 index 0000000000000000000000000000000000000000..cc24b5e796342f7733f59933362d46a0df2be112 --- /dev/null +++ b/crates/collab/src/db/tables/user_feature.rs @@ -0,0 +1,42 @@ +use sea_orm::entity::prelude::*; + +use crate::db::{FlagId, UserId}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "user_features")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + #[sea_orm(primary_key)] + pub feature_id: FlagId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::feature_flag::Entity", + from = "Column::FeatureId", + to = "super::feature_flag::Column::Id" + )] + Flag, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Flag.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 36a0888a62ed243904598d1386f8567fe5b821fd..ee961006cbbf74b019141c0973aca18d73309012 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,5 +1,6 @@ mod buffer_tests; mod db_tests; +mod feature_flag_tests; use super::*; use gpui::executor::Background; diff --git a/crates/collab/src/db/tests/feature_flag_tests.rs b/crates/collab/src/db/tests/feature_flag_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..9d5f039747c18fb6cfae77191654ba5b4584e21e --- /dev/null +++ b/crates/collab/src/db/tests/feature_flag_tests.rs @@ -0,0 +1,60 @@ +use crate::{ + db::{Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; + +test_both_dbs!( + test_get_user_flags, + test_get_user_flags_postgres, + test_get_user_flags_sqlite +); + +async fn test_get_user_flags(db: &Arc) { + let user_1 = db + .create_user( + &format!("user1@example.com"), + false, + NewUserParams { + github_login: format!("user1"), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_2 = db + .create_user( + &format!("user2@example.com"), + false, + NewUserParams { + github_login: format!("user2"), + github_user_id: 2, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + const CHANNELS_ALPHA: &'static str = "channels-alpha"; + const NEW_SEARCH: &'static str = "new-search"; + + let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap(); + let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap(); + + db.add_user_flag(user_1, channels_flag).await.unwrap(); + db.add_user_flag(user_1, search_flag).await.unwrap(); + + db.add_user_flag(user_2, channels_flag).await.unwrap(); + + let mut user_1_flags = db.get_user_flags(user_1).await.unwrap(); + user_1_flags.sort(); + assert_eq!(user_1_flags, &[CHANNELS_ALPHA, NEW_SEARCH]); + + let mut user_2_flags = db.get_user_flags(user_2).await.unwrap(); + user_2_flags.sort(); + assert_eq!(user_2_flags, &[CHANNELS_ALPHA]); +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 18587c2ba8f590f3646a3a7de6d4121ffe35586d..6b44711c42f4a37eea15c437879650a7c269aad5 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2609,20 +2609,19 @@ async fn get_private_user_info( response: Response, session: Session, ) -> Result<()> { - let metrics_id = session - .db() - .await - .get_user_metrics_id(session.user_id) - .await?; - let user = session - .db() - .await + let db = session.db().await; + + let metrics_id = db.get_user_metrics_id(session.user_id).await?; + let user = db .get_user_by_id(session.user_id) .await? .ok_or_else(|| anyhow!("user not found"))?; + let flags = db.get_user_flags(session.user_id).await?; + response.send(proto::GetPrivateUserInfoResponse { metrics_id, staff: user.admin, + flags, })?; Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ce47830af22e8203f33aaa86f3f953a86065ad94..e0356ebb9ad4c5cb46ecf9f16ece6dc91d6ecffb 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1111,6 +1111,7 @@ message GetPrivateUserInfo {} message GetPrivateUserInfoResponse { string metrics_id = 1; bool staff = 2; + repeated string flags = 3; } // Entities diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index bc9dd6f80ba039bb705e3d1518c737ba56c969b9..d64cbae92993ec2b092fcebdcf48d20f2c7449d6 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 61; +pub const PROTOCOL_VERSION: u32 = 62; From a3b2c03b1720a9dbda35ff8084a850d8b4560cd8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 16:13:12 -0700 Subject: [PATCH 097/142] Fix bugs in autoscroll with 'fit' strategy * Scroll to the newest cursor if all cursors can't fit in the viewport. * Refuse to layout an editor less tall than one line height. Co-authored-by: Nathan --- crates/editor/src/editor_tests.rs | 68 ++++++++++++++++++++++++ crates/editor/src/element.rs | 7 +-- crates/editor/src/scroll/autoscroll.rs | 73 +++++++++++++++----------- 3 files changed, 111 insertions(+), 37 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a2a561402f904be26d7a3a8d316519fb60387a19..25a5d45282d580aab5a93bf8ad3750250486e42a 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1434,6 +1434,74 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_autoscroll(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, cx| { + editor.set_vertical_scroll_margin(2, cx); + editor.style(cx).text.line_height(cx.font_cache()) + }); + + let window = cx.window; + window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); + }); + + // Add a cursor below the visible area. Since both cursors cannot fit + // on screen, the editor autoscrolls to reveal the newest cursor, and + // allows the vertical scroll margin below that cursor. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); + }); + + // Move down. The editor cursor scrolls down to track the newest cursor. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); + }); + + // Add a cursor above the visible area. Since both cursors fit on screen, + // the editor scrolls to show both. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(1, 0)..Point::new(1, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); + }); +} + #[gpui::test] async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3ba807308c6718e2fcbf7e8edf0f9c9f9d08824e..1f77979adb90c164c0305ff96cd95693aff50f32 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2113,14 +2113,11 @@ impl Element for EditorElement { scroll_height .min(constraint.max_along(Axis::Vertical)) .max(constraint.min_along(Axis::Vertical)) + .max(line_height) .min(line_height * max_lines as f32), ) } else if let EditorMode::SingleLine = snapshot.mode { - size.set_y( - line_height - .min(constraint.max_along(Axis::Vertical)) - .max(constraint.min_along(Axis::Vertical)), - ) + size.set_y(line_height.max(constraint.min_along(Axis::Vertical))) } else if size.y().is_infinite() { size.set_y(scroll_height); } diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index e83e2286b1f4809d777c72257eda0e7471508ccf..ffada50179fa233b12e4a02b4fed6e52bcf137ca 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -65,47 +65,52 @@ impl Editor { self.set_scroll_position(scroll_position, cx); } - let (autoscroll, local) = - if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() { - autoscroll - } else { - return false; - }; - - let first_cursor_top; - let last_cursor_bottom; + let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { + return false; + }; + + let mut target_top; + let mut target_bottom; if let Some(highlighted_rows) = &self.highlighted_rows { - first_cursor_top = highlighted_rows.start as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else if autoscroll == Autoscroll::newest() { - let newest_selection = self.selections.newest::(cx); - first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; - last_cursor_bottom = first_cursor_top + 1.; + target_top = highlighted_rows.start as f32; + target_bottom = target_top + 1.; } else { let selections = self.selections.all::(cx); - first_cursor_top = selections + target_top = selections .first() .unwrap() .head() .to_display_point(&display_map) .row() as f32; - last_cursor_bottom = selections + target_bottom = selections .last() .unwrap() .head() .to_display_point(&display_map) .row() as f32 + 1.0; + + // If the selections can't all fit on screen, scroll to the newest. + if autoscroll == Autoscroll::newest() + || autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines + { + let newest_selection_top = selections + .iter() + .max_by_key(|s| s.id) + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + target_top = newest_selection_top; + target_bottom = newest_selection_top + 1.; + } } let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { 0. } else { - ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() + ((visible_lines - (target_bottom - target_top)) / 2.0).floor() }; - if margin < 0.0 { - return false; - } let strategy = match autoscroll { Autoscroll::Strategy(strategy) => strategy, @@ -113,8 +118,8 @@ impl Editor { let last_autoscroll = &self.scroll_manager.last_autoscroll; if let Some(last_autoscroll) = last_autoscroll { if self.scroll_manager.anchor.offset == last_autoscroll.0 - && first_cursor_top == last_autoscroll.1 - && last_cursor_bottom == last_autoscroll.2 + && target_top == last_autoscroll.1 + && target_bottom == last_autoscroll.2 { last_autoscroll.3.next() } else { @@ -129,37 +134,41 @@ impl Editor { match strategy { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); - let target_top = (first_cursor_top - margin).max(0.0); - let target_bottom = last_cursor_bottom + margin; + let target_top = (target_top - margin).max(0.0); + let target_bottom = target_bottom + margin; let start_row = scroll_position.y(); let end_row = start_row + visible_lines; - if target_top < start_row { + let needs_scroll_up = target_top < start_row; + let needs_scroll_down = target_bottom >= end_row; + + if needs_scroll_up && !needs_scroll_down { scroll_position.set_y(target_top); self.set_scroll_position_internal(scroll_position, local, true, cx); - } else if target_bottom >= end_row { + } + if !needs_scroll_up && needs_scroll_down { scroll_position.set_y(target_bottom - visible_lines); self.set_scroll_position_internal(scroll_position, local, true, cx); } } AutoscrollStrategy::Center => { - scroll_position.set_y((first_cursor_top - margin).max(0.0)); + scroll_position.set_y((target_top - margin).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } AutoscrollStrategy::Top => { - scroll_position.set_y((first_cursor_top).max(0.0)); + scroll_position.set_y((target_top).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } AutoscrollStrategy::Bottom => { - scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0)); + scroll_position.set_y((target_bottom - visible_lines).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } } self.scroll_manager.last_autoscroll = Some(( self.scroll_manager.anchor.offset, - first_cursor_top, - last_cursor_bottom, + target_top, + target_bottom, strategy, )); From 2495d6581efcef080cc33eaa3928ee9d487dbc34 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 26 Aug 2023 01:31:52 +0200 Subject: [PATCH 098/142] Un serialize project search (#2857) This is the first batch of improvements to current project search. There are few things we can do better still, but I want to get this out in next Preview. Most of the slowness at this point seems to stem from updating UI too often. Release Notes: - Improved project search by making it report results sooner. --------- Co-authored-by: Julia Risley --- crates/collab/src/tests/integration_tests.rs | 21 +- .../src/tests/randomized_integration_tests.rs | 18 +- crates/editor/src/multi_buffer.rs | 96 ++- crates/project/src/project.rs | 583 ++++++++++++------ crates/project/src/project_tests.rs | 11 +- crates/search/src/project_search.rs | 62 +- 6 files changed, 490 insertions(+), 301 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 9bee8d434cd9ccb6d0fa252e2badc49be99a54d4..b1227b9501a4990b9afb68aa72d22efd355defd7 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4,7 +4,7 @@ use crate::{ }; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, @@ -4821,15 +4821,16 @@ async fn test_project_search( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Perform a search as the guest. - let results = project_b - .update(cx_b, |project, cx| { - project.search( - SearchQuery::text("world", false, false, Vec::new(), Vec::new()), - cx, - ) - }) - .await - .unwrap(); + let mut results = HashMap::default(); + let mut search_rx = project_b.update(cx_b, |project, cx| { + project.search( + SearchQuery::text("world", false, false, Vec::new(), Vec::new()), + cx, + ) + }); + while let Some((buffer, ranges)) = search_rx.next().await { + results.entry(buffer).or_insert(ranges); + } let mut ranges_by_path = results .into_iter() diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 3557843828cacb81b69b88d24514da6936b83139..814f248b6dc722fa67f2af2dd70c66f54af3a57a 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::{anyhow, Result}; use call::ActiveCall; use client::RECEIVE_TIMEOUT; -use collections::BTreeMap; +use collections::{BTreeMap, HashMap}; use editor::Bias; use fs::{repository::GitFileStatus, FakeFs, Fs as _}; use futures::StreamExt as _; @@ -722,7 +722,7 @@ async fn apply_client_operation( if detach { "detaching" } else { "awaiting" } ); - let search = project.update(cx, |project, cx| { + let mut search = project.update(cx, |project, cx| { project.search( SearchQuery::text(query, false, false, Vec::new(), Vec::new()), cx, @@ -730,15 +730,13 @@ async fn apply_client_operation( }); drop(project); let search = cx.background().spawn(async move { - search - .await - .map_err(|err| anyhow!("search request failed: {:?}", err)) + let mut results = HashMap::default(); + while let Some((buffer, ranges)) = search.next().await { + results.entry(buffer).or_insert(ranges); + } + results }); - if detach { - cx.update(|cx| search.detach_and_log_err(cx)); - } else { - search.await?; - } + search.await; } ClientOperation::WriteFsEntry { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index b9bf4f52a1c50e3b31aaf91da6586b9cfea7c958..5c0d8b641cac5731508beae589a499727aac0dd8 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -6,7 +6,7 @@ use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; use git::diff::DiffHunk; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{AppContext, Entity, ModelContext, ModelHandle}; pub use language::Completion; use language::{ char_kind, @@ -788,59 +788,59 @@ impl MultiBuffer { pub fn stream_excerpts_with_context_lines( &mut self, - excerpts: Vec<(ModelHandle, Vec>)>, + buffer: ModelHandle, + ranges: Vec>, context_line_count: u32, cx: &mut ModelContext, - ) -> (Task<()>, mpsc::Receiver>) { + ) -> mpsc::Receiver> { let (mut tx, rx) = mpsc::channel(256); - let task = cx.spawn(|this, mut cx| async move { - for (buffer, ranges) in excerpts { - let (buffer_id, buffer_snapshot) = - buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); - - let mut excerpt_ranges = Vec::new(); - let mut range_counts = Vec::new(); - cx.background() - .scoped(|scope| { - scope.spawn(async { - let (ranges, counts) = - build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); - excerpt_ranges = ranges; - range_counts = counts; - }); - }) - .await; - - let mut ranges = ranges.into_iter(); - let mut range_counts = range_counts.into_iter(); - for excerpt_ranges in excerpt_ranges.chunks(100) { - let excerpt_ids = this.update(&mut cx, |this, cx| { - this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + cx.spawn(|this, mut cx| async move { + let (buffer_id, buffer_snapshot) = + buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); + + let mut excerpt_ranges = Vec::new(); + let mut range_counts = Vec::new(); + cx.background() + .scoped(|scope| { + scope.spawn(async { + let (ranges, counts) = + build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); + excerpt_ranges = ranges; + range_counts = counts; }); + }) + .await; - for (excerpt_id, range_count) in - excerpt_ids.into_iter().zip(range_counts.by_ref()) - { - for range in ranges.by_ref().take(range_count) { - let start = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.start, - }; - let end = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.end, - }; - if tx.send(start..end).await.is_err() { - break; - } + let mut ranges = ranges.into_iter(); + let mut range_counts = range_counts.into_iter(); + for excerpt_ranges in excerpt_ranges.chunks(100) { + let excerpt_ids = this.update(&mut cx, |this, cx| { + this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + }); + + for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref()) + { + for range in ranges.by_ref().take(range_count) { + let start = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.start, + }; + let end = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.end, + }; + if tx.send(start..end).await.is_err() { + break; } } } } - }); - (task, rx) + }) + .detach(); + + rx } pub fn push_excerpts( @@ -4438,7 +4438,7 @@ mod tests { async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let (task, anchor_ranges) = multibuffer.update(cx, |multibuffer, cx| { + let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); let ranges = vec![ snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)), @@ -4446,12 +4446,10 @@ mod tests { snapshot.anchor_before(Point::new(15, 0)) ..snapshot.anchor_before(Point::new(15, 0)), ]; - multibuffer.stream_excerpts_with_context_lines(vec![(buffer.clone(), ranges)], 2, cx) + multibuffer.stream_excerpts_with_context_lines(buffer.clone(), ranges, 2, cx) }); let anchor_ranges = anchor_ranges.collect::>().await; - // Ensure task is finished when stream completes. - task.await; let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); assert_eq!( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c7765bf55a70b4829cf43575b7679db94ff44065..bb18a41ad42a58cd8271519cdfd074b93603ce25 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,8 +26,8 @@ use futures::{ }; use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui::{ - AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext, - ModelHandle, Task, WeakModelHandle, + executor::Background, AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, + ModelContext, ModelHandle, Task, WeakModelHandle, }; use itertools::Itertools; use language::{ @@ -37,11 +37,11 @@ use language::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, }, - range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel, - Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _, - Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt, - Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, - ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, + CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, + File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, + OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, + ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -57,8 +57,8 @@ use serde::Serialize; use settings::SettingsStore; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; +use smol::channel::{Receiver, Sender}; use std::{ - cell::RefCell, cmp::{self, Ordering}, convert::TryInto, hash::Hash, @@ -67,7 +67,6 @@ use std::{ ops::Range, path::{self, Component, Path, PathBuf}, process::Stdio, - rc::Rc, str, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -525,6 +524,28 @@ impl FormatTrigger { } } } +#[derive(Clone, Debug, PartialEq)] +enum SearchMatchCandidate { + OpenBuffer { + buffer: ModelHandle, + // This might be an unnamed file without representation on filesystem + path: Option>, + }, + Path { + worktree_id: WorktreeId, + path: Arc, + }, +} + +type SearchMatchCandidateIndex = usize; +impl SearchMatchCandidate { + fn path(&self) -> Option> { + match self { + SearchMatchCandidate::OpenBuffer { path, .. } => path.clone(), + SearchMatchCandidate::Path { path, .. } => Some(path.clone()), + } + } +} impl Project { pub fn init_settings(cx: &mut AppContext) { @@ -5099,187 +5120,11 @@ impl Project { &self, query: SearchQuery, cx: &mut ModelContext, - ) -> Task, Vec>>>> { + ) -> Receiver<(ModelHandle, Vec>)> { if self.is_local() { - let snapshots = self - .visible_worktrees(cx) - .filter_map(|tree| { - let tree = tree.read(cx).as_local()?; - Some(tree.snapshot()) - }) - .collect::>(); - - let background = cx.background().clone(); - let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); - if path_count == 0 { - return Task::ready(Ok(Default::default())); - } - let workers = background.num_cpus().min(path_count); - let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); - cx.background() - .spawn({ - let fs = self.fs.clone(); - let background = cx.background().clone(); - let query = query.clone(); - async move { - let fs = &fs; - let query = &query; - let matching_paths_tx = &matching_paths_tx; - let paths_per_worker = (path_count + workers - 1) / workers; - let snapshots = &snapshots; - background - .scoped(|scope| { - for worker_ix in 0..workers { - let worker_start_ix = worker_ix * paths_per_worker; - let worker_end_ix = worker_start_ix + paths_per_worker; - scope.spawn(async move { - let mut snapshot_start_ix = 0; - let mut abs_path = PathBuf::new(); - for snapshot in snapshots { - let snapshot_end_ix = - snapshot_start_ix + snapshot.visible_file_count(); - if worker_end_ix <= snapshot_start_ix { - break; - } else if worker_start_ix > snapshot_end_ix { - snapshot_start_ix = snapshot_end_ix; - continue; - } else { - let start_in_snapshot = worker_start_ix - .saturating_sub(snapshot_start_ix); - let end_in_snapshot = - cmp::min(worker_end_ix, snapshot_end_ix) - - snapshot_start_ix; - - for entry in snapshot - .files(false, start_in_snapshot) - .take(end_in_snapshot - start_in_snapshot) - { - if matching_paths_tx.is_closed() { - break; - } - let matches = if query - .file_matches(Some(&entry.path)) - { - abs_path.clear(); - abs_path.push(&snapshot.abs_path()); - abs_path.push(&entry.path); - if let Some(file) = - fs.open_sync(&abs_path).await.log_err() - { - query.detect(file).unwrap_or(false) - } else { - false - } - } else { - false - }; - - if matches { - let project_path = - (snapshot.id(), entry.path.clone()); - if matching_paths_tx - .send(project_path) - .await - .is_err() - { - break; - } - } - } - - snapshot_start_ix = snapshot_end_ix; - } - } - }); - } - }) - .await; - } - }) - .detach(); - - let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); - let open_buffers = self - .opened_buffers - .values() - .filter_map(|b| b.upgrade(cx)) - .collect::>(); - cx.spawn(|this, cx| async move { - for buffer in &open_buffers { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer.clone(), snapshot)).await?; - } - - let open_buffers = Rc::new(RefCell::new(open_buffers)); - while let Some(project_path) = matching_paths_rx.next().await { - if buffers_tx.is_closed() { - break; - } - - let this = this.clone(); - let open_buffers = open_buffers.clone(); - let buffers_tx = buffers_tx.clone(); - cx.spawn(|mut cx| async move { - if let Some(buffer) = this - .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) - .await - .log_err() - { - if open_buffers.borrow_mut().insert(buffer.clone()) { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; - } - } - - Ok::<_, anyhow::Error>(()) - }) - .detach(); - } - - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); - - let background = cx.background().clone(); - cx.background().spawn(async move { - let query = &query; - let mut matched_buffers = Vec::new(); - for _ in 0..workers { - matched_buffers.push(HashMap::default()); - } - background - .scoped(|scope| { - for worker_matched_buffers in matched_buffers.iter_mut() { - let mut buffers_rx = buffers_rx.clone(); - scope.spawn(async move { - while let Some((buffer, snapshot)) = buffers_rx.next().await { - let buffer_matches = if query.file_matches( - snapshot.file().map(|file| file.path().as_ref()), - ) { - query - .search(&snapshot, None) - .await - .iter() - .map(|range| { - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end) - }) - .collect() - } else { - Vec::new() - }; - if !buffer_matches.is_empty() { - worker_matched_buffers - .insert(buffer.clone(), buffer_matches); - } - } - }); - } - }) - .await; - Ok(matched_buffers.into_iter().flatten().collect()) - }) + self.search_local(query, cx) } else if let Some(project_id) = self.remote_id() { + let (tx, rx) = smol::channel::unbounded(); let request = self.client.request(query.to_proto(project_id)); cx.spawn(|this, mut cx| async move { let response = request.await?; @@ -5303,13 +5148,303 @@ impl Project { .or_insert(Vec::new()) .push(start..end) } - Ok(result) + for (buffer, ranges) in result { + let _ = tx.send((buffer, ranges)).await; + } + Result::<(), anyhow::Error>::Ok(()) }) + .detach_and_log_err(cx); + rx } else { - Task::ready(Ok(Default::default())) + unimplemented!(); } } + pub fn search_local( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Receiver<(ModelHandle, Vec>)> { + // Local search is split into several phases. + // TL;DR is that we do 2 passes; initial pass to pick files which contain at least one match + // and the second phase that finds positions of all the matches found in the candidate files. + // The Receiver obtained from this function returns matches sorted by buffer path. Files without a buffer path are reported first. + // + // It gets a bit hairy though, because we must account for files that do not have a persistent representation + // on FS. Namely, if you have an untitled buffer or unsaved changes in a buffer, we want to scan that too. + // + // 1. We initialize a queue of match candidates and feed all opened buffers into it (== unsaved files / untitled buffers). + // Then, we go through a worktree and check for files that do match a predicate. If the file had an opened version, we skip the scan + // of FS version for that file altogether - after all, what we have in memory is more up-to-date than what's in FS. + // 2. At this point, we have a list of all potentially matching buffers/files. + // We sort that list by buffer path - this list is retained for later use. + // We ensure that all buffers are now opened and available in project. + // 3. We run a scan over all the candidate buffers on multiple background threads. + // We cannot assume that there will even be a match - while at least one match + // is guaranteed for files obtained from FS, the buffers we got from memory (unsaved files/unnamed buffers) might not have a match at all. + // There is also an auxilliary background thread responsible for result gathering. + // This is where the sorted list of buffers comes into play to maintain sorted order; Whenever this background thread receives a notification (buffer has/doesn't have matches), + // it keeps it around. It reports matches in sorted order, though it accepts them in unsorted order as well. + // As soon as the match info on next position in sorted order becomes available, it reports it (if it's a match) or skips to the next + // entry - which might already be available thanks to out-of-order processing. + // + // We could also report matches fully out-of-order, without maintaining a sorted list of matching paths. + // This however would mean that project search (that is the main user of this function) would have to do the sorting itself, on the go. + // This isn't as straightforward as running an insertion sort sadly, and would also mean that it would have to care about maintaining match index + // in face of constantly updating list of sorted matches. + // Meanwhile, this implementation offers index stability, since the matches are already reported in a sorted order. + let snapshots = self + .visible_worktrees(cx) + .filter_map(|tree| { + let tree = tree.read(cx).as_local()?; + Some(tree.snapshot()) + }) + .collect::>(); + + let background = cx.background().clone(); + let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); + if path_count == 0 { + let (_, rx) = smol::channel::bounded(1024); + return rx; + } + let workers = background.num_cpus().min(path_count); + let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024); + let mut unnamed_files = vec![]; + let opened_buffers = self + .opened_buffers + .iter() + .filter_map(|(_, b)| { + let buffer = b.upgrade(cx)?; + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + if let Some(path) = snapshot.file().map(|file| file.path()) { + Some((path.clone(), (buffer, snapshot))) + } else { + unnamed_files.push(buffer); + None + } + }) + .collect(); + cx.background() + .spawn(Self::background_search( + unnamed_files, + opened_buffers, + cx.background().clone(), + self.fs.clone(), + workers, + query.clone(), + path_count, + snapshots, + matching_paths_tx, + )) + .detach(); + + let (buffers, buffers_rx) = Self::sort_candidates_and_open_buffers(matching_paths_rx, cx); + let background = cx.background().clone(); + let (result_tx, result_rx) = smol::channel::bounded(1024); + cx.background() + .spawn(async move { + let Ok(buffers) = buffers.await else { + return; + }; + + let buffers_len = buffers.len(); + if buffers_len == 0 { + return; + } + let query = &query; + let (finished_tx, mut finished_rx) = smol::channel::unbounded(); + background + .scoped(|scope| { + #[derive(Clone)] + struct FinishedStatus { + entry: Option<(ModelHandle, Vec>)>, + buffer_index: SearchMatchCandidateIndex, + } + + for _ in 0..workers { + let finished_tx = finished_tx.clone(); + let mut buffers_rx = buffers_rx.clone(); + scope.spawn(async move { + while let Some((entry, buffer_index)) = buffers_rx.next().await { + let buffer_matches = if let Some((_, snapshot)) = entry.as_ref() + { + if query.file_matches( + snapshot.file().map(|file| file.path().as_ref()), + ) { + query + .search(&snapshot, None) + .await + .iter() + .map(|range| { + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .collect() + } else { + Vec::new() + } + } else { + Vec::new() + }; + + let status = if !buffer_matches.is_empty() { + let entry = if let Some((buffer, _)) = entry.as_ref() { + Some((buffer.clone(), buffer_matches)) + } else { + None + }; + FinishedStatus { + entry, + buffer_index, + } + } else { + FinishedStatus { + entry: None, + buffer_index, + } + }; + if finished_tx.send(status).await.is_err() { + break; + } + } + }); + } + // Report sorted matches + scope.spawn(async move { + let mut current_index = 0; + let mut scratch = vec![None; buffers_len]; + while let Some(status) = finished_rx.next().await { + debug_assert!( + scratch[status.buffer_index].is_none(), + "Got match status of position {} twice", + status.buffer_index + ); + let index = status.buffer_index; + scratch[index] = Some(status); + while current_index < buffers_len { + let Some(current_entry) = scratch[current_index].take() else { + // We intentionally **do not** increment `current_index` here. When next element arrives + // from `finished_rx`, we will inspect the same position again, hoping for it to be Some(_) + // this time. + break; + }; + if let Some(entry) = current_entry.entry { + result_tx.send(entry).await.log_err(); + } + current_index += 1; + } + if current_index == buffers_len { + break; + } + } + }); + }) + .await; + }) + .detach(); + result_rx + } + /// Pick paths that might potentially contain a match of a given search query. + async fn background_search( + unnamed_buffers: Vec>, + opened_buffers: HashMap, (ModelHandle, BufferSnapshot)>, + background: Arc, + fs: Arc, + workers: usize, + query: SearchQuery, + path_count: usize, + snapshots: Vec, + matching_paths_tx: Sender, + ) { + let fs = &fs; + let query = &query; + let matching_paths_tx = &matching_paths_tx; + let snapshots = &snapshots; + let paths_per_worker = (path_count + workers - 1) / workers; + for buffer in unnamed_buffers { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: None, + }) + .await + .log_err(); + } + for (path, (buffer, _)) in opened_buffers.iter() { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: Some(path.clone()), + }) + .await + .log_err(); + } + background + .scoped(|scope| { + for worker_ix in 0..workers { + let worker_start_ix = worker_ix * paths_per_worker; + let worker_end_ix = worker_start_ix + paths_per_worker; + let unnamed_buffers = opened_buffers.clone(); + scope.spawn(async move { + let mut snapshot_start_ix = 0; + let mut abs_path = PathBuf::new(); + for snapshot in snapshots { + let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count(); + if worker_end_ix <= snapshot_start_ix { + break; + } else if worker_start_ix > snapshot_end_ix { + snapshot_start_ix = snapshot_end_ix; + continue; + } else { + let start_in_snapshot = + worker_start_ix.saturating_sub(snapshot_start_ix); + let end_in_snapshot = + cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix; + + for entry in snapshot + .files(false, start_in_snapshot) + .take(end_in_snapshot - start_in_snapshot) + { + if matching_paths_tx.is_closed() { + break; + } + if unnamed_buffers.contains_key(&entry.path) { + continue; + } + let matches = if query.file_matches(Some(&entry.path)) { + abs_path.clear(); + abs_path.push(&snapshot.abs_path()); + abs_path.push(&entry.path); + if let Some(file) = fs.open_sync(&abs_path).await.log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + } + } else { + false + }; + + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }; + if matching_paths_tx.send(project_path).await.is_err() { + break; + } + } + } + + snapshot_start_ix = snapshot_end_ix; + } + } + }); + } + }) + .await; + } + // TODO: Wire this up to allow selecting a server? fn request_lsp( &self, @@ -5384,6 +5519,61 @@ impl Project { Task::ready(Ok(Default::default())) } + fn sort_candidates_and_open_buffers( + mut matching_paths_rx: Receiver, + cx: &mut ModelContext, + ) -> ( + futures::channel::oneshot::Receiver>, + Receiver<( + Option<(ModelHandle, BufferSnapshot)>, + SearchMatchCandidateIndex, + )>, + ) { + let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); + let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); + cx.spawn(|this, cx| async move { + let mut buffers = vec![]; + while let Some(entry) = matching_paths_rx.next().await { + buffers.push(entry); + } + buffers.sort_by_key(|candidate| candidate.path()); + let matching_paths = buffers.clone(); + let _ = sorted_buffers_tx.send(buffers); + for (index, candidate) in matching_paths.into_iter().enumerate() { + if buffers_tx.is_closed() { + break; + } + let this = this.clone(); + let buffers_tx = buffers_tx.clone(); + cx.spawn(|mut cx| async move { + let buffer = match candidate { + SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), + SearchMatchCandidate::Path { worktree_id, path } => this + .update(&mut cx, |this, cx| { + this.open_buffer((worktree_id, path), cx) + }) + .await + .log_err(), + }; + if let Some(buffer) = buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx + .send((Some((buffer, snapshot)), index)) + .await + .log_err(); + } else { + buffers_tx.send((None, index)).await.log_err(); + } + + Ok::<_, anyhow::Error>(()) + }) + .detach(); + } + }) + .detach(); + (sorted_buffers_rx, buffers_rx) + } + pub fn find_or_create_local_worktree( &mut self, abs_path: impl AsRef, @@ -7006,17 +7196,17 @@ impl Project { ) -> Result { let peer_id = envelope.original_sender_id()?; let query = SearchQuery::from_proto(envelope.payload)?; - let result = this - .update(&mut cx, |this, cx| this.search(query, cx)) - .await?; + let mut result = this.update(&mut cx, |this, cx| this.search(query, cx)); - this.update(&mut cx, |this, cx| { + cx.spawn(|mut cx| async move { let mut locations = Vec::new(); - for (buffer, ranges) in result { + while let Some((buffer, ranges)) = result.next().await { for range in ranges { let start = serialize_anchor(&range.start); let end = serialize_anchor(&range.end); - let buffer_id = this.create_buffer_for_peer(&buffer, peer_id, cx); + let buffer_id = this.update(&mut cx, |this, cx| { + this.create_buffer_for_peer(&buffer, peer_id, cx) + }); locations.push(proto::Location { buffer_id, start: Some(start), @@ -7026,6 +7216,7 @@ impl Project { } Ok(proto::SearchProjectResponse { locations }) }) + .await } async fn handle_open_buffer_for_symbol( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a504900c83e9178f9ee6cbcf6a66e77a8fa44c0d..7c5983a0a90de924384c37871720246dce5f6983 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3953,11 +3953,12 @@ async fn search( query: SearchQuery, cx: &mut gpui::TestAppContext, ) -> Result>>> { - let results = project - .update(cx, |project, cx| project.search(query, cx)) - .await?; - - Ok(results + let mut search_rx = project.update(cx, |project, cx| project.search(query, cx)); + let mut result = HashMap::default(); + while let Some((buffer, range)) = search_rx.next().await { + result.entry(buffer).or_insert(range); + } + Ok(result .into_iter() .map(|(buffer, ranges)| { buffer.read_with(cx, |buffer, _| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d7633a45e49af1a90a86d6bdce2dba95d209d74b..6364183877b7e1f2bff8ea26b6244a0ed57c6be9 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -185,28 +185,26 @@ impl ProjectSearch { self.active_query = Some(query); self.match_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { - let matches = search.await.log_err()?; + let mut matches = search; let this = this.upgrade(&cx)?; - let mut matches = matches.into_iter().collect::>(); - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { this.match_ranges.clear(); + this.excerpts.update(cx, |this, cx| this.clear(cx)); this.no_results = Some(true); - matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - excerpts.stream_excerpts_with_context_lines(matches, 1, cx) - }) }); - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + while let Some((buffer, anchors)) = matches.next().await { + let mut ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); - cx.notify(); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) + }) }); + + while let Some(range) = ranges.next().await { + this.update(&mut cx, |this, _| this.match_ranges.push(range)); + } + this.update(&mut cx, |_, cx| cx.notify()); } this.update(&mut cx, |this, cx| { @@ -238,29 +236,31 @@ impl ProjectSearch { self.no_results = Some(true); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let results = search?.await.log_err()?; + let matches = results + .into_iter() + .map(|result| (result.buffer, vec![result.range.start..result.range.start])); - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { this.excerpts.update(cx, |excerpts, cx| { excerpts.clear(cx); - - let matches = results - .into_iter() - .map(|result| (result.buffer, vec![result.range.start..result.range.start])) - .collect(); - - excerpts.stream_excerpts_with_context_lines(matches, 3, cx) }) }); - - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + for (buffer, ranges) in matches { + let mut match_ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); - cx.notify(); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) + }) }); + while let Some(match_range) = match_ranges.next().await { + this.update(&mut cx, |this, cx| { + this.match_ranges.push(match_range); + while let Ok(Some(match_range)) = match_ranges.try_next() { + this.match_ranges.push(match_range); + } + cx.notify(); + }); + } } this.update(&mut cx, |this, cx| { From ddd7ab116f981d2871f266d63721aaca5942fa15 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 00:05:39 +0300 Subject: [PATCH 099/142] Do not convert lsp::Location of hint labels before resolve --- crates/editor/src/link_go_to_definition.rs | 130 ++++++++----- crates/project/src/lsp_command.rs | 208 ++++++--------------- crates/project/src/project.rs | 29 ++- crates/rpc/proto/zed.proto | 5 +- 4 files changed, 163 insertions(+), 209 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 926c0d6ddeb588bf133d032e9492c2249e9711eb..8d46194f4226e527f5ff9da681e44957a05d7474 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -4,13 +4,15 @@ use crate::{ hover_popover::{self, InlayHover}, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; +use anyhow::Context; use gpui::{Task, ViewContext}; -use language::{Bias, ToOffset}; +use language::{point_from_lsp, Bias, LanguageServerName, ToOffset}; +use lsp::LanguageServerId; use project::{ HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, ResolveState, }; -use std::ops::Range; +use std::{ops::Range, sync::Arc}; use util::TryFutureExt; #[derive(Debug, Default)] @@ -24,7 +26,7 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(InlayRange, LocationLink), + InlayHint(InlayRange, lsp::Location, LanguageServerId), None, } @@ -38,7 +40,7 @@ pub struct InlayRange { #[derive(Debug, Clone)] pub enum TriggerPoint { Text(Anchor), - InlayHint(InlayRange, LocationLink), + InlayHint(InlayRange, lsp::Location, LanguageServerId), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -61,12 +63,12 @@ impl DocumentRange { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } - (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => { range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } (DocumentRange::Inlay(_), TriggerPoint::Text(_)) - | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false, } } } @@ -75,7 +77,7 @@ impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(coordinates, _) => &coordinates.inlay_position, + TriggerPoint::InlayHint(coordinates, _, _) => &coordinates.inlay_position, } } @@ -88,7 +90,7 @@ impl TriggerPoint { LinkDefinitionKind::Symbol } } - TriggerPoint::InlayHint(_, _) => LinkDefinitionKind::Type, + TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type, } } } @@ -110,7 +112,9 @@ pub fn update_go_to_definition_link( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id) => { + Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id)) + } GoToDefinitionTrigger::None => None, }; @@ -277,35 +281,25 @@ pub fn update_inlay_link_and_hover_points( ); hover_updated = true; } - if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = - cached_hint.position.buffer_id.and_then(|buffer_id| { - editor.buffer().read(cx).buffer(buffer_id) - }) - { - go_to_definition_updated = true; - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::InlayHint( - InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - LocationLink { - origin: Some(Location { - buffer, - range: cached_hint.position - ..cached_hint.position, - }), - target: location, - }, - ), - cmd_held, - shift_held, - cx, - ); - } + if let Some((language_server_id, location)) = + hovered_hint_part.location + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + location, + language_server_id, + ), + cmd_held, + shift_held, + cx, + ); } } } @@ -415,7 +409,6 @@ pub fn show_link_definition( let end = snapshot .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - DocumentRange::Text(start..end) }) }), @@ -423,10 +416,59 @@ pub fn show_link_definition( ) }) } - TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( - Some(DocumentRange::Inlay(trigger_source.clone())), - vec![trigger_target.clone()], - )), + TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => { + let target = match project.update(&mut cx, |project, cx| { + let language_server_name = project + .language_server_for_buffer(buffer.read(cx), *server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + *server_id, + language_server_name, + cx, + ) + }) + }) { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }); + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + + target.map(|target| { + ( + Some(DocumentRange::Inlay(trigger_source.clone())), + vec![LocationLink { + origin: Some(Location { + buffer: buffer.clone(), + range: trigger_source.inlay_position.text_anchor + ..trigger_source.inlay_position.text_anchor, + }), + target, + }], + ) + }) + } }; this.update(&mut cx, |this, cx| { @@ -479,7 +521,7 @@ pub fn show_link_definition( ..snapshot.anchor_after(offset_range.end), ) } - TriggerPoint::InlayHint(inlay_coordinates, _) => { + TriggerPoint::InlayHint(inlay_coordinates, _, _) => { DocumentRange::Inlay(inlay_coordinates) } }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8ca2571869f4575262cd9453015dec5ef9dfcd7d..8239cf869067043d25549c5cfa74337e5211271d 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,6 +1,6 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, - InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Item, Location, LocationLink, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; @@ -14,8 +14,8 @@ use language::{ point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, - CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, - Transaction, Unclipped, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1787,7 +1787,6 @@ impl LspCommand for OnTypeFormatting { impl InlayHints { pub async fn lsp_to_project_hint( lsp_hint: lsp::InlayHint, - project: &ModelHandle, buffer_handle: &ModelHandle, server_id: LanguageServerId, resolve_state: ResolveState, @@ -1809,15 +1808,9 @@ impl InlayHints { buffer.anchor_after(position) } }); - let label = Self::lsp_inlay_label_to_project( - &buffer_handle, - project, - server_id, - lsp_hint.label, - cx, - ) - .await - .context("lsp to project inlay hint conversion")?; + let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id) + .await + .context("lsp to project inlay hint conversion")?; let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { false } else { @@ -1847,72 +1840,14 @@ impl InlayHints { } async fn lsp_inlay_label_to_project( - buffer: &ModelHandle, - project: &ModelHandle, - server_id: LanguageServerId, lsp_label: lsp::InlayHintLabel, - cx: &mut AsyncAppContext, + server_id: LanguageServerId, ) -> anyhow::Result { let label = match lsp_label { lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), lsp::InlayHintLabel::LabelParts(lsp_parts) => { - let mut parts_data = Vec::with_capacity(lsp_parts.len()); - buffer.update(cx, |buffer, cx| { - for lsp_part in lsp_parts { - let location_buffer_task = match &lsp_part.location { - Some(lsp_location) => { - let location_buffer_task = project.update(cx, |project, cx| { - let language_server_name = project - .language_server_for_buffer(buffer, server_id, cx) - .map(|(_, lsp_adapter)| { - LanguageServerName(Arc::from(lsp_adapter.name())) - }); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - server_id, - language_server_name, - cx, - ) - }) - }); - Some(lsp_location.clone()).zip(location_buffer_task) - } - None => None, - }; - - parts_data.push((lsp_part, location_buffer_task)); - } - }); - - let mut parts = Vec::with_capacity(parts_data.len()); - for (lsp_part, location_buffer_task) in parts_data { - let location = match location_buffer_task { - Some((lsp_location, target_buffer_handle_task)) => { - let target_buffer_handle = target_buffer_handle_task - .await - .context("resolving location for label part buffer")?; - let range = cx.read(|cx| { - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - }); - Some(Location { - buffer: target_buffer_handle, - range, - }) - } - None => None, - }; - + let mut parts = Vec::with_capacity(lsp_parts.len()); + for lsp_part in lsp_parts { parts.push(InlayHintLabelPart { value: lsp_part.value, tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { @@ -1929,7 +1864,7 @@ impl InlayHints { }) } }), - location, + location: Some(server_id).zip(lsp_part.location), }); } InlayHintLabel::LabelParts(parts) @@ -1939,7 +1874,7 @@ impl InlayHints { Ok(label) } - pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { + pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint { let (state, lsp_resolve_state) = match response_hint.resolve_state { ResolveState::Resolved => (0, None), ResolveState::CanResolve(server_id, resolve_data) => ( @@ -1969,7 +1904,11 @@ impl InlayHints { 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 { + parts: label_parts.into_iter().map(|label_part| { + let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string()); + let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + proto::InlayHintLabelPart { value: label_part.value, tooltip: label_part.tooltip.map(|tooltip| { let proto_tooltip = match tooltip { @@ -1981,12 +1920,11 @@ impl InlayHints { }; 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() + location_url, + location_range_start, + location_range_end, + language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64), + }}).collect() }) } }), @@ -1994,16 +1932,12 @@ impl InlayHints { 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::String(s) => proto::inlay_hint_tooltip::Content::Value(s), InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - is_markdown: markup_content.kind == HoverBlockKind::Markdown, - value: markup_content.value, - }, - ) + proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }) } }; proto::InlayHintTooltip { @@ -2014,16 +1948,7 @@ impl InlayHints { } } - pub async fn proto_to_project_hint( - message_hint: proto::InlayHint, - project: &ModelHandle, - cx: &mut AsyncAppContext, - ) -> anyhow::Result { - let buffer_id = message_hint - .position - .as_ref() - .and_then(|location| location.buffer_id) - .context("missing buffer id")?; + pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result { let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) }); @@ -2064,9 +1989,6 @@ impl InlayHints { proto::inlay_hint_label::Label::LabelParts(parts) => { let mut label_parts = Vec::new(); for part in parts.parts { - let buffer = project - .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx)) - .await?; label_parts.push(InlayHintLabelPart { value: part.value, tooltip: part.tooltip.map(|tooltip| match tooltip.content { @@ -2087,19 +2009,35 @@ impl InlayHints { }), None => InlayHintLabelPartTooltip::String(String::new()), }), - location: match part.location { - Some(location) => Some(Location { - range: location - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")? - ..location - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?, - buffer, - }), - None => None, + location: { + match part + .location_url + .zip( + part.location_range_start.and_then(|start| { + Some(start..part.location_range_end?) + }), + ) + .zip(part.language_server_id) + { + Some(((uri, range), server_id)) => Some(( + LanguageServerId(server_id as usize), + lsp::Location { + uri: lsp::Url::parse(&uri) + .context("invalid uri in hint part {part:?}")?, + range: lsp::Range::new( + point_to_lsp(PointUtf16::new( + range.start.row, + range.start.column, + )), + point_to_lsp(PointUtf16::new( + range.end.row, + range.end.column, + )), + ), + }, + )), + None => None, + } }, }); } @@ -2132,12 +2070,7 @@ impl InlayHints { }) } - pub fn project_to_lsp_hint( - hint: InlayHint, - project: &ModelHandle, - snapshot: &BufferSnapshot, - cx: &AsyncAppContext, - ) -> lsp::InlayHint { + pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint { lsp::InlayHint { position: point_to_lsp(hint.position.to_point_utf16(snapshot)), kind: hint.kind.map(|kind| match kind { @@ -2190,22 +2123,7 @@ impl InlayHints { } }) }), - location: part.location.and_then(|location| { - let (path, location_snapshot) = cx.read(|cx| { - let buffer = location.buffer.read(cx); - let project_path = buffer.project_path(cx)?; - let location_snapshot = buffer.snapshot(); - let path = project.read(cx).absolute_path(&project_path, cx); - path.zip(Some(location_snapshot)) - })?; - Some(lsp::Location::new( - lsp::Url::from_file_path(path).unwrap(), - range_to_lsp( - location.range.start.to_point_utf16(&location_snapshot) - ..location.range.end.to_point_utf16(&location_snapshot), - ), - )) - }), + location: part.location.map(|(_, location)| location), command: None, }) .collect(), @@ -2299,12 +2217,10 @@ impl LspCommand for InlayHints { ResolveState::Resolved }; - let project = project.clone(); let buffer = buffer.clone(); cx.spawn(|mut cx| async move { InlayHints::lsp_to_project_hint( lsp_hint, - &project, &buffer, server_id, resolve_state, @@ -2359,12 +2275,12 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - cx: &mut AppContext, + _: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| InlayHints::project_to_proto_hint(response_hint, cx)) + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint)) .collect(), version: serialize_version(buffer_version), } @@ -2373,7 +2289,7 @@ impl LspCommand for InlayHints { async fn response_from_proto( self, message: proto::InlayHintsResponse, - project: ModelHandle, + _: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, ) -> anyhow::Result> { @@ -2385,7 +2301,7 @@ impl LspCommand for InlayHints { let mut hints = Vec::new(); for message_hint in message.hints { - hints.push(InlayHints::proto_to_project_hint(message_hint, &project, &mut cx).await?); + hints.push(InlayHints::proto_to_project_hint(message_hint)?); } Ok(hints) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bb18a41ad42a58cd8271519cdfd074b93603ce25..547c7dcc959b32423699b250d03ec13991da03fb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -369,7 +369,7 @@ pub enum InlayHintLabel { pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, - pub location: Option, + pub location: Option<(LanguageServerId, lsp::Location)>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1708,7 +1708,7 @@ impl Project { } /// LanguageServerName is owned, because it is inserted into a map - fn open_local_buffer_via_lsp( + pub fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, language_server_id: LanguageServerId, @@ -5069,16 +5069,15 @@ impl Project { } let buffer_snapshot = buffer.snapshot(); - cx.spawn(|project, mut cx| async move { + cx.spawn(|_, mut cx| async move { let resolve_task = lang_server.request::( - InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), + InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), ); let resolved_hint = resolve_task .await .context("inlay hint resolve LSP request")?; let resolved_hint = InlayHints::lsp_to_project_hint( resolved_hint, - &project, &buffer_handle, server_id, ResolveState::Resolved, @@ -5094,19 +5093,16 @@ impl Project { project_id, buffer_id: buffer_handle.read(cx).remote_id(), language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint.clone(), cx)), + hint: Some(InlayHints::project_to_proto_hint(hint.clone())), }; - cx.spawn(|project, mut cx| async move { + cx.spawn(|_, _| async move { let response = client .request(request) .await .context("inlay hints proto request")?; match response.hint { - Some(resolved_hint) => { - InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) - .await - .context("inlay hints proto resolve response conversion") - } + Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint) + .context("inlay hints proto resolve response conversion"), None => Ok(hint), } }) @@ -7091,8 +7087,7 @@ impl Project { .payload .hint .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); - let hint = InlayHints::proto_to_project_hint(proto_hint, &this, &mut cx) - .await + let hint = InlayHints::proto_to_project_hint(proto_hint) .context("resolved proto inlay hint conversion")?; let buffer = this.update(&mut cx, |this, cx| { this.opened_buffers @@ -7111,10 +7106,8 @@ impl Project { }) .await .context("inlay hints fetch")?; - let resolved_hint = cx.read(|cx| InlayHints::project_to_proto_hint(response_hint, cx)); - Ok(proto::ResolveInlayHintResponse { - hint: Some(resolved_hint), + hint: Some(InlayHints::project_to_proto_hint(response_hint)), }) } @@ -7882,7 +7875,7 @@ impl Project { self.language_servers_for_buffer(buffer, cx).next() } - fn language_server_for_buffer( + pub fn language_server_for_buffer( &self, buffer: &Buffer, server_id: LanguageServerId, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ce47830af22e8203f33aaa86f3f953a86065ad94..3d855c9c694295aa9942779a7ff65b7e1844d3a0 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -773,7 +773,10 @@ message InlayHintLabelParts { message InlayHintLabelPart { string value = 1; InlayHintLabelPartTooltip tooltip = 2; - Location location = 3; + optional string location_url = 3; + PointUtf16 location_range_start = 4; + PointUtf16 location_range_end = 5; + optional uint64 language_server_id = 6; } message InlayHintTooltip { From 665d86ea737c07abf16b9c9f00e24cb6214e0332 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 01:55:17 +0300 Subject: [PATCH 100/142] Defer navigation target buffer opening --- crates/editor/src/editor.rs | 222 +++++++++++++++------ crates/editor/src/element.rs | 8 +- crates/editor/src/link_go_to_definition.rs | 190 +++++++----------- 3 files changed, 246 insertions(+), 174 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c5ff1f027da7faac89bbcbf596d501fffbeae2cb..79186d6e8ca7ec9ff47a74552d45582951f55752 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -23,7 +23,7 @@ pub mod test; use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; use client::{ClickhouseEvent, TelemetrySettings}; use clock::{Global, ReplicaId}; @@ -60,21 +60,24 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, - Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, - OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, + LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, + TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, DocumentRange, InlayRange, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange, + LinkGoToDefinitionState, }; use log::error; +use lsp::LanguageServerId; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use ordered_float::OrderedFloat; -use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; +use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, @@ -6551,7 +6554,14 @@ impl Editor { cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move { let definitions = definitions.await?; editor.update(&mut cx, |editor, cx| { - editor.navigate_to_definitions(definitions, split, cx); + editor.navigate_to_definitions( + definitions + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), + split, + cx, + ); })?; Ok::<(), anyhow::Error>(()) }) @@ -6560,7 +6570,7 @@ impl Editor { pub fn navigate_to_definitions( &mut self, - mut definitions: Vec, + mut definitions: Vec, split: bool, cx: &mut ViewContext, ) { @@ -6571,67 +6581,167 @@ impl Editor { // If there is one definition, just open it directly if definitions.len() == 1 { let definition = definitions.pop().unwrap(); - let range = definition - .target - .range - .to_offset(definition.target.buffer.read(cx)); - - let range = self.range_for_match(&range); - if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); - } else { - cx.window_context().defer(move |cx| { - let target_editor: ViewHandle = workspace.update(cx, |workspace, cx| { - if split { - workspace.split_project_item(definition.target.buffer.clone(), cx) + let target_task = match definition { + GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))), + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + self.compute_target_location(lsp_location, server_id, cx) + } + }; + cx.spawn(|editor, mut cx| async move { + let target = target_task.await.context("target resolution task")?; + if let Some(target) = target { + editor.update(&mut cx, |editor, cx| { + let range = target.range.to_offset(target.buffer.read(cx)); + let range = editor.range_for_match(&range); + if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }); } else { - workspace.open_project_item(definition.target.buffer.clone(), cx) + cx.window_context().defer(move |cx| { + let target_editor: ViewHandle = + workspace.update(cx, |workspace, cx| { + if split { + workspace.split_project_item(target.buffer.clone(), cx) + } else { + workspace.open_project_item(target.buffer.clone(), cx) + } + }); + target_editor.update(cx, |target_editor, cx| { + // When selecting a definition in a different buffer, disable the nav history + // to avoid creating a history entry at the previous cursor location. + pane.update(cx, |pane, _| pane.disable_history()); + target_editor.change_selections( + Some(Autoscroll::fit()), + cx, + |s| { + s.select_ranges([range]); + }, + ); + pane.update(cx, |pane, _| pane.enable_history()); + }); + }); } - }); - target_editor.update(cx, |target_editor, cx| { - // When selecting a definition in a different buffer, disable the nav history - // to avoid creating a history entry at the previous cursor location. - pane.update(cx, |pane, _| pane.disable_history()); - target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); - pane.update(cx, |pane, _| pane.enable_history()); - }); - }); - } + }) + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); } else if !definitions.is_empty() { let replica_id = self.replica_id(cx); - cx.window_context().defer(move |cx| { - let title = definitions - .iter() - .find(|definition| definition.origin.is_some()) - .and_then(|definition| { - definition.origin.as_ref().map(|origin| { - let buffer = origin.buffer.read(cx); - format!( - "Definitions for {}", - buffer - .text_for_range(origin.range.clone()) - .collect::() - ) - }) + cx.spawn(|editor, mut cx| async move { + let (title, location_tasks) = editor + .update(&mut cx, |editor, cx| { + let title = definitions + .iter() + .find_map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + link.origin.as_ref().map(|origin| { + let buffer = origin.buffer.read(cx); + format!( + "Definitions for {}", + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }) + } + GoToDefinitionLink::InlayHint(_, _) => None, + }) + .unwrap_or("Definitions".to_string()); + let location_tasks = definitions + .into_iter() + .map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + Task::Ready(Some(Ok(Some(link.target)))) + } + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + editor.compute_target_location(lsp_location, server_id, cx) + } + }) + .collect::>(); + (title, location_tasks) }) - .unwrap_or("Definitions".to_owned()); - let locations = definitions + .context("location tasks preparation")?; + + let locations = futures::future::join_all(location_tasks) + .await .into_iter() - .map(|definition| definition.target) - .collect(); - workspace.update(cx, |workspace, cx| { + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + workspace.update(&mut cx, |workspace, cx| { Self::open_locations_in_multibuffer( workspace, locations, replica_id, title, split, cx, ) }); - }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } + fn compute_target_location( + &self, + lsp_location: lsp::Location, + server_id: LanguageServerId, + cx: &mut ViewContext, + ) -> Task>> { + let Some(project) = self.project.clone() else { + return Task::Ready(Some(Ok(None))); + }; + + cx.spawn(move |editor, mut cx| async move { + let location_task = editor.update(&mut cx, |editor, cx| { + project.update(cx, |project, cx| { + let language_server_name = + editor.buffer.read(cx).as_singleton().and_then(|buffer| { + project + .language_server_for_buffer(buffer.read(cx), server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }) + })?; + let location = match location_task { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = { + target_buffer_handle.update(&mut cx, |target_buffer, _| { + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }) + }; + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + Ok(location) + }) + } + pub fn find_all_references( workspace: &mut Workspace, _: &FindAllReferences, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5b52cc3c7259b7b19cc2d1f96122617fed5e1bb8..684f92a96a8ee8b11e218960d6ac556d75187bfe 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -395,9 +395,7 @@ impl EditorElement { update_go_to_definition_link( editor, - point - .map(GoToDefinitionTrigger::Text) - .unwrap_or(GoToDefinitionTrigger::None), + point.map(GoToDefinitionTrigger::Text), cmd, shift, cx, @@ -468,7 +466,7 @@ impl EditorElement { Some(point) => { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(point), + Some(GoToDefinitionTrigger::Text(point)), cmd, shift, cx, @@ -487,7 +485,7 @@ impl EditorElement { } } } else { - update_go_to_definition_link(editor, GoToDefinitionTrigger::None, cmd, shift, cx); + update_go_to_definition_link(editor, None, cmd, shift, cx); hover_at(editor, None, cx); } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 8d46194f4226e527f5ff9da681e44957a05d7474..ceeda0829accacad8e6d7f84afedc474eb05fff7 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -4,15 +4,14 @@ use crate::{ hover_popover::{self, InlayHover}, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; -use anyhow::Context; use gpui::{Task, ViewContext}; -use language::{point_from_lsp, Bias, LanguageServerName, ToOffset}; +use language::{Bias, ToOffset}; use lsp::LanguageServerId; use project::{ - HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, - LocationLink, ResolveState, + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, + ResolveState, }; -use std::{ops::Range, sync::Arc}; +use std::ops::Range; use util::TryFutureExt; #[derive(Debug, Default)] @@ -20,14 +19,19 @@ pub struct LinkGoToDefinitionState { pub last_trigger_point: Option, pub symbol_range: Option, pub kind: Option, - pub definitions: Vec, + pub definitions: Vec, pub task: Option>>, } pub enum GoToDefinitionTrigger { Text(DisplayPoint), InlayHint(InlayRange, lsp::Location, LanguageServerId), - None, +} + +#[derive(Debug, Clone)] +pub enum GoToDefinitionLink { + Text(LocationLink), + InlayHint(lsp::Location, LanguageServerId), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -97,7 +101,7 @@ impl TriggerPoint { pub fn update_go_to_definition_link( editor: &mut Editor, - origin: GoToDefinitionTrigger, + origin: Option, cmd_held: bool, shift_held: bool, cx: &mut ViewContext, @@ -107,15 +111,15 @@ pub fn update_go_to_definition_link( // Store new mouse point as an anchor let snapshot = editor.snapshot(cx); let trigger_point = match origin { - GoToDefinitionTrigger::Text(p) => { + Some(GoToDefinitionTrigger::Text(p)) => { Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id) => { + Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => { Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id)) } - GoToDefinitionTrigger::None => None, + None => None, }; // If the new point is the same as the previously stored one, return early @@ -287,7 +291,7 @@ pub fn update_inlay_link_and_hover_points( go_to_definition_updated = true; update_go_to_definition_link( editor, - GoToDefinitionTrigger::InlayHint( + Some(GoToDefinitionTrigger::InlayHint( InlayRange { inlay_position: hovered_hint.position, highlight_start: part_range.start, @@ -295,7 +299,7 @@ pub fn update_inlay_link_and_hover_points( }, location, language_server_id, - ), + )), cmd_held, shift_held, cx, @@ -311,13 +315,7 @@ pub fn update_inlay_link_and_hover_points( } if !go_to_definition_updated { - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::None, - cmd_held, - shift_held, - cx, - ); + update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); } if !hover_updated { hover_popover::hover_at(editor, None, cx); @@ -412,63 +410,20 @@ pub fn show_link_definition( DocumentRange::Text(start..end) }) }), - definition_result, - ) - }) - } - TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => { - let target = match project.update(&mut cx, |project, cx| { - let language_server_name = project - .language_server_for_buffer(buffer.read(cx), *server_id, cx) - .map(|(_, lsp_adapter)| { - LanguageServerName(Arc::from(lsp_adapter.name())) - }); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - *server_id, - language_server_name, - cx, - ) - }) - }) { - Some(task) => Some({ - let target_buffer_handle = task.await.context("open local buffer")?; - let range = cx.read(|cx| { - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - }); - Location { - buffer: target_buffer_handle, - range, - } - }), - None => None, - }; - - target.map(|target| { - ( - Some(DocumentRange::Inlay(trigger_source.clone())), - vec![LocationLink { - origin: Some(Location { - buffer: buffer.clone(), - range: trigger_source.inlay_position.text_anchor - ..trigger_source.inlay_position.text_anchor, - }), - target, - }], + definition_result + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), ) }) } + TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some(( + Some(DocumentRange::Inlay(*trigger_source)), + vec![GoToDefinitionLink::InlayHint( + lsp_location.clone(), + *server_id, + )], + )), }; this.update(&mut cx, |this, cx| { @@ -488,43 +443,52 @@ pub fn show_link_definition( // the current location. let any_definition_does_not_contain_current_location = definitions.iter().any(|definition| { - let target = &definition.target; - if target.buffer == buffer { - let range = &target.range; - // Expand range by one character as lsp definition ranges include positions adjacent - // but not contained by the symbol range - let start = buffer_snapshot.clip_offset( - range.start.to_offset(&buffer_snapshot).saturating_sub(1), - Bias::Left, - ); - let end = buffer_snapshot.clip_offset( - range.end.to_offset(&buffer_snapshot) + 1, - Bias::Right, - ); - let offset = buffer_position.to_offset(&buffer_snapshot); - !(start <= offset && end >= offset) - } else { - true + match &definition { + GoToDefinitionLink::Text(link) => { + if link.target.buffer == buffer { + let range = &link.target.range; + // Expand range by one character as lsp definition ranges include positions adjacent + // but not contained by the symbol range + let start = buffer_snapshot.clip_offset( + range + .start + .to_offset(&buffer_snapshot) + .saturating_sub(1), + Bias::Left, + ); + let end = buffer_snapshot.clip_offset( + range.end.to_offset(&buffer_snapshot) + 1, + Bias::Right, + ); + let offset = buffer_position.to_offset(&buffer_snapshot); + !(start <= offset && end >= offset) + } else { + true + } + } + GoToDefinitionLink::InlayHint(_, _) => true, } }); if any_definition_does_not_contain_current_location { // Highlight symbol using theme link definition highlight style let style = theme::current(cx).editor.link_definition; - let highlight_range = symbol_range.unwrap_or_else(|| match trigger_point { - TriggerPoint::Text(trigger_anchor) => { - let snapshot = &snapshot.buffer_snapshot; - // If no symbol range returned from language server, use the surrounding word. - let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - DocumentRange::Text( - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end), - ) - } - TriggerPoint::InlayHint(inlay_coordinates, _, _) => { - DocumentRange::Inlay(inlay_coordinates) - } - }); + let highlight_range = + symbol_range.unwrap_or_else(|| match &trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = + snapshot.surrounding_word(*trigger_anchor); + DocumentRange::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(inlay_coordinates, _, _) => { + DocumentRange::Inlay(*inlay_coordinates) + } + }); match highlight_range { DocumentRange::Text(text_range) => this @@ -689,7 +653,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, true, cx, @@ -801,7 +765,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -841,7 +805,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -869,7 +833,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -892,7 +856,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), false, false, cx, @@ -957,7 +921,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -977,7 +941,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -1079,7 +1043,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, From b2b091879057bf9be983bb8d305ef36ed64387f3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 02:13:36 +0300 Subject: [PATCH 101/142] Consider padding during hint highlight range mapping --- crates/editor/src/hover_popover.rs | 15 +++++++++++++-- crates/editor/src/link_go_to_definition.rs | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 3ce936ae8275b03e4f75abe2cd39ae11f7938214..89e8d8e246cfe7dee57fb51ec16ab817bf88d69f 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -57,19 +57,30 @@ pub struct InlayHover { pub fn find_hovered_hint_part( label_parts: Vec, + padding_left: bool, + padding_right: bool, hint_range: Range, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; let mut part_start = hint_range.start; - for part in label_parts { + let last_label_part_index = label_parts.len() - 1; + for (i, part) in label_parts.into_iter().enumerate() { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; part_start.0 += part_len; } else { - return Some((part, part_start..InlayOffset(part_start.0 + part_len))); + let mut part_end = InlayOffset(part_start.0 + part_len); + if padding_left { + part_start.0 += 1; + part_end.0 += 1; + } + if padding_right && i == last_label_part_index { + part_end.0 -= 1; + } + return Some((part, part_start..part_end)); } } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ceeda0829accacad8e6d7f84afedc474eb05fff7..2ada17de06ec5d02eb6df49f3b65f734ff8f7167 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -218,6 +218,15 @@ pub fn update_inlay_link_and_hover_points( ResolveState::Resolved => { match cached_hint.label { project::InlayHintLabel::String(_) => { + let mut highlight_start = hint_start_offset; + let mut highlight_end = hint_end_offset; + if cached_hint.padding_left { + highlight_start.0 += 1; + highlight_end.0 += 1; + } + if cached_hint.padding_right { + highlight_end.0 -= 1; + } if let Some(tooltip) = cached_hint.tooltip { hover_popover::hover_at_inlay( editor, @@ -238,8 +247,8 @@ pub fn update_inlay_link_and_hover_points( triggered_from: hovered_offset, range: InlayRange { inlay_position: hovered_hint.position, - highlight_start: hint_start_offset, - highlight_end: hint_end_offset, + highlight_start, + highlight_end, }, }, cx, @@ -251,6 +260,8 @@ pub fn update_inlay_link_and_hover_points( if let Some((hovered_hint_part, part_range)) = hover_popover::find_hovered_hint_part( label_parts, + cached_hint.padding_left, + cached_hint.padding_right, hint_start_offset..hint_end_offset, hovered_offset, ) From e6c4802488f39ebc3f5ce6c3d400fba54c038342 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 02:44:19 +0300 Subject: [PATCH 102/142] Properly clip request offsets --- crates/editor/src/inlay_hint_cache.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index a4b91e826d06083a0979b6c4ffd45225d8fb41c2..0067c832d36bf87758563774206186411d94075a 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -19,6 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use sum_tree::Bias; use text::ToOffset; use util::post_inc; @@ -632,8 +633,8 @@ fn determine_query_ranges( return None; } else { vec![ - buffer.anchor_before(excerpt_visible_range.start) - ..buffer.anchor_after(excerpt_visible_range.end), + buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)), ] }; @@ -651,8 +652,8 @@ fn determine_query_ranges( .min(full_excerpt_range_end_offset) .min(buffer.len()); vec![ - buffer.anchor_before(after_visible_range_start) - ..buffer.anchor_after(after_range_end_offset), + buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)), ] }; @@ -668,8 +669,8 @@ fn determine_query_ranges( .saturating_sub(excerpt_visible_len) .max(full_excerpt_range_start_offset); vec![ - buffer.anchor_before(before_range_start_offset) - ..buffer.anchor_after(before_visible_range_end), + buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)), ] }; From 81c64647e82777a6286bbf7534b7aa41d47beaf5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 03:00:53 +0300 Subject: [PATCH 103/142] Fix the test --- crates/editor/src/link_go_to_definition.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 2ada17de06ec5d02eb6df49f3b65f734ff8f7167..280993dd26499cf529ef62e905c140bcc35982bc 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -975,6 +975,7 @@ mod tests { // the cached location instead Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); + cx.foreground().run_until_parked(); cx.assert_editor_state(indoc! {" fn «testˇ»() { do_work(); } fn do_work() { test(); } From 74565ed0b86afa85fb8e26cb35219182ecf667b7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 25 Aug 2023 17:00:53 -0700 Subject: [PATCH 104/142] Add feature flags handling to the client, rewrite staff mode to a trait extension style --- Cargo.lock | 28 +++---- Cargo.toml | 2 +- crates/channel/Cargo.toml | 2 +- crates/client/Cargo.toml | 2 +- crates/client/src/telemetry.rs | 2 - crates/client/src/user.rs | 35 ++++---- crates/collab_ui/Cargo.toml | 2 +- crates/collab_ui/src/collab_panel.rs | 35 ++++---- .../{staff_mode => feature_flags}/Cargo.toml | 4 +- crates/feature_flags/src/feature_flags.rs | 79 +++++++++++++++++++ crates/rpc/src/rpc.rs | 2 +- crates/settings/Cargo.toml | 2 +- crates/staff_mode/src/staff_mode.rs | 36 --------- crates/theme_selector/Cargo.toml | 2 +- crates/theme_selector/src/theme_selector.rs | 4 +- crates/zed/Cargo.toml | 2 +- crates/zed/src/languages/json.rs | 4 +- crates/zed/src/main.rs | 7 +- 18 files changed, 143 insertions(+), 107 deletions(-) rename crates/{staff_mode => feature_flags}/Cargo.toml (71%) create mode 100644 crates/feature_flags/src/feature_flags.rs delete mode 100644 crates/staff_mode/src/staff_mode.rs diff --git a/Cargo.lock b/Cargo.lock index 8197f883c0615e0e1293d16ec832c31e3c842031..bfdb1b6092d1b221697a356a845e2db3ee4d6b02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1200,6 +1200,7 @@ dependencies = [ "client", "collections", "db", + "feature_flags", "futures 0.3.28", "gpui", "image", @@ -1215,7 +1216,6 @@ dependencies = [ "serde_derive", "settings", "smol", - "staff_mode", "sum_tree", "tempfile", "text", @@ -1374,6 +1374,7 @@ dependencies = [ "async-tungstenite", "collections", "db", + "feature_flags", "futures 0.3.28", "gpui", "image", @@ -1388,7 +1389,6 @@ dependencies = [ "serde_derive", "settings", "smol", - "staff_mode", "sum_tree", "tempfile", "text", @@ -1528,6 +1528,7 @@ dependencies = [ "context_menu", "db", "editor", + "feature_flags", "feedback", "futures 0.3.28", "fuzzy", @@ -1543,7 +1544,6 @@ dependencies = [ "serde", "serde_derive", "settings", - "staff_mode", "theme", "theme_selector", "util", @@ -2529,6 +2529,14 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "feature_flags" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", +] + [[package]] name = "feedback" version = "0.1.0" @@ -6834,6 +6842,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "feature_flags", "fs", "futures 0.3.28", "gpui", @@ -6849,7 +6858,6 @@ dependencies = [ "serde_json_lenient", "smallvec", "sqlez", - "staff_mode", "toml 0.5.11", "tree-sitter", "tree-sitter-json 0.19.0", @@ -7284,14 +7292,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "staff_mode" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -7672,6 +7672,7 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "editor", + "feature_flags", "fs", "fuzzy", "gpui", @@ -7681,7 +7682,6 @@ dependencies = [ "postage", "settings", "smol", - "staff_mode", "theme", "util", "workspace", @@ -9726,6 +9726,7 @@ dependencies = [ "diagnostics", "editor", "env_logger 0.9.3", + "feature_flags", "feedback", "file_finder", "fs", @@ -9772,7 +9773,6 @@ dependencies = [ "simplelog", "smallvec", "smol", - "staff_mode", "sum_tree", "tempdir", "terminal_view", diff --git a/Cargo.toml b/Cargo.toml index 0fb8f0b6b718013b65a999cd8620282fd6979a6b..5938ecb40240765844c1849662b082afeae07a3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ members = [ "crates/snippet", "crates/sqlez", "crates/sqlez_macros", - "crates/staff_mode", + "crates/feature_flags", "crates/sum_tree", "crates/terminal", "crates/text", diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 0978462a1a8a8a66760992edc4967b5b451603bc..c2191fdfa3edaaf0824e5e59ed974a7c53030ccd 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -21,7 +21,7 @@ rpc = { path = "../rpc" } text = { path = "../text" } language = { path = "../language" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } anyhow.workspace = true diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 64d8f02c8ae1eba2525abca8a4847edb30a458e8..e3038e5bcc49bd41b756062b676e00f4f355867a 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -19,7 +19,7 @@ util = { path = "../util" } rpc = { path = "../rpc" } text = { path = "../text" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } anyhow.workspace = true diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 48886377ba56ad046027da5cf7a754bdc22cea72..9cc5d13af0c72d84bdad54c88d97e2e9ce2586df 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -135,8 +135,6 @@ impl Telemetry { } } - /// This method takes the entire TelemetrySettings struct in order to force client code - /// to pull the struct out of the settings global. Do not remove! pub fn set_authenticated_user_info( self: &Arc, metrics_id: Option, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1dc384da1725c6d58b92aff7477ed516ef69590f..5f13aa40acee9063bfd90c10b43044ff40952db2 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,11 +1,11 @@ use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; +use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; -use staff_mode::StaffMode; use std::sync::{Arc, Weak}; use util::http::HttpClient; use util::TryFutureExt as _; @@ -145,26 +145,23 @@ impl UserStore { let fetch_metrics_id = client.request(proto::GetPrivateUserInfo {}).log_err(); let (user, info) = futures::join!(fetch_user, fetch_metrics_id); - cx.read(|cx| { - client.telemetry.set_authenticated_user_info( - info.as_ref().map(|info| info.metrics_id.clone()), - info.as_ref().map(|info| info.staff).unwrap_or(false), - cx, - ) - }); - cx.update(|cx| { - cx.update_default_global(|staff_mode: &mut StaffMode, _| { - if !staff_mode.0 { - *staff_mode = StaffMode( - info.as_ref() - .map(|info| info.staff) - .unwrap_or_default(), - ) - } - () + if let Some(info) = info { + cx.update(|cx| { + cx.update_flags(info.staff, info.flags); + client.telemetry.set_authenticated_user_info( + Some(info.metrics_id.clone()), + info.staff, + cx, + ) }); - }); + } else { + cx.read(|cx| { + client + .telemetry + .set_authenticated_user_info(None, false, cx) + }); + } current_user_tx.send(user).await.ok(); diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 1ecb4b84227b066ab959997c128bfd96cec6055d..da32308558f7c7e8279c420961f8d42d9356d37b 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -40,7 +40,7 @@ picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} settings = { path = "../settings" } -staff_mode = {path = "../staff_mode"} +feature_flags = {path = "../feature_flags"} theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } vcs_menu = { path = "../vcs_menu" } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 411a3a2598c052dfb92f4df438effa1c1e57270a..0593bfcb1f279be0ce9fd7fed4dd2672d1813cc4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -9,6 +9,8 @@ use client::{proto::PeerId, Client, Contact, User, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; + +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use futures::StreamExt; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -33,7 +35,6 @@ use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings} use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; -use staff_mode::StaffMode; use std::{borrow::Cow, mem, sync::Arc}; use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; @@ -182,9 +183,9 @@ pub struct CollabPanel { } #[derive(Serialize, Deserialize)] -struct SerializedChannelsPanel { +struct SerializedCollabPanel { width: Option, - collapsed_channels: Vec, + collapsed_channels: Option>, } #[derive(Debug)] @@ -472,9 +473,10 @@ impl CollabPanel { })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); - this.subscriptions.push( - cx.observe_global::(move |this, cx| this.update_entries(true, cx)), - ); + this.subscriptions + .push(cx.observe_flag::(move |_, this, cx| { + this.update_entries(true, cx) + })); this.subscriptions.push(cx.subscribe( &this.channel_store, |this, _channel_store, e, cx| match e { @@ -510,7 +512,7 @@ impl CollabPanel { .log_err() .flatten() { - Some(serde_json::from_str::(&panel)?) + Some(serde_json::from_str::(&panel)?) } else { None }; @@ -520,7 +522,9 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; - panel.collapsed_channels = serialized_panel.collapsed_channels; + panel.collapsed_channels = serialized_panel + .collapsed_channels + .unwrap_or_else(|| Vec::new()); cx.notify(); }); } @@ -537,9 +541,9 @@ impl CollabPanel { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), - serde_json::to_string(&SerializedChannelsPanel { + serde_json::to_string(&SerializedCollabPanel { width, - collapsed_channels, + collapsed_channels: Some(collapsed_channels), })?, ) .await?; @@ -672,7 +676,8 @@ impl CollabPanel { } let mut request_entries = Vec::new(); - if self.include_channels_section(cx) { + + if cx.has_flag::() { self.entries.push(ListEntry::Header(Section::Channels, 0)); if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { @@ -1909,14 +1914,6 @@ impl CollabPanel { .into_any() } - fn include_channels_section(&self, cx: &AppContext) -> bool { - if cx.has_global::() { - cx.global::().0 - } else { - false - } - } - fn deploy_channel_context_menu( &mut self, position: Option, diff --git a/crates/staff_mode/Cargo.toml b/crates/feature_flags/Cargo.toml similarity index 71% rename from crates/staff_mode/Cargo.toml rename to crates/feature_flags/Cargo.toml index 2193bd11b127d94840ed22c1bd7d4e0fb2b8310b..af273fe4033c7fbca36df2ccc8a2daae86eec19b 100644 --- a/crates/staff_mode/Cargo.toml +++ b/crates/feature_flags/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "staff_mode" +name = "feature_flags" version = "0.1.0" edition = "2021" publish = false [lib] -path = "src/staff_mode.rs" +path = "src/feature_flags.rs" [dependencies] gpui = { path = "../gpui" } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs new file mode 100644 index 0000000000000000000000000000000000000000..d14152b04c6155b37091adabd32ab68bcdbf6cdd --- /dev/null +++ b/crates/feature_flags/src/feature_flags.rs @@ -0,0 +1,79 @@ +use gpui::{AppContext, Subscription, ViewContext}; + +#[derive(Default)] +struct FeatureFlags { + flags: Vec, + staff: bool, +} + +impl FeatureFlags { + fn has_flag(&self, flag: &str) -> bool { + self.staff || self.flags.iter().find(|f| f.as_str() == flag).is_some() + } +} + +pub trait FeatureFlag { + const NAME: &'static str; +} + +pub enum ChannelsAlpha {} + +impl FeatureFlag for ChannelsAlpha { + const NAME: &'static str = "channels_alpha"; +} + +pub trait FeatureFlagViewExt { + fn observe_flag(&mut self, callback: F) -> Subscription + where + F: Fn(bool, &mut V, &mut ViewContext) + 'static; +} + +impl FeatureFlagViewExt for ViewContext<'_, '_, V> { + fn observe_flag(&mut self, callback: F) -> Subscription + where + F: Fn(bool, &mut V, &mut ViewContext) + 'static, + { + self.observe_global::(move |v, cx| { + let feature_flags = cx.global::(); + callback(feature_flags.has_flag(::NAME), v, cx); + }) + } +} + +pub trait FeatureFlagAppExt { + fn update_flags(&mut self, staff: bool, flags: Vec); + fn set_staff(&mut self, staff: bool); + fn has_flag(&self) -> bool; + fn is_staff(&self) -> bool; +} + +impl FeatureFlagAppExt for AppContext { + fn update_flags(&mut self, staff: bool, flags: Vec) { + self.update_default_global::(|feature_flags, _| { + feature_flags.staff = staff; + feature_flags.flags = flags; + }) + } + + fn set_staff(&mut self, staff: bool) { + self.update_default_global::(|feature_flags, _| { + feature_flags.staff = staff; + }) + } + + fn has_flag(&self) -> bool { + if self.has_global::() { + self.global::().has_flag(T::NAME) + } else { + false + } + } + + fn is_staff(&self) -> bool { + if self.has_global::() { + return self.global::().staff; + } else { + false + } + } +} diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index d64cbae92993ec2b092fcebdcf48d20f2c7449d6..bc9dd6f80ba039bb705e3d1518c737ba56c969b9 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 62; +pub const PROTOCOL_VERSION: u32 = 61; diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 06b81a0c61139ce0bd0a0c58a6101b8a043393bb..f89b80902d0f8e12aade715e7903e8191a8445dc 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -16,7 +16,7 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } sqlez = { path = "../sqlez" } fs = { path = "../fs" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } util = { path = "../util" } anyhow.workspace = true diff --git a/crates/staff_mode/src/staff_mode.rs b/crates/staff_mode/src/staff_mode.rs deleted file mode 100644 index 49fadc0b2cccdd64fdf22e8fed1a887de009749e..0000000000000000000000000000000000000000 --- a/crates/staff_mode/src/staff_mode.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gpui::AppContext; - -#[derive(Debug, Default)] -pub struct StaffMode(pub bool); - -impl std::ops::Deref for StaffMode { - type Target = bool; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Despite what the type system requires me to tell you, the init function will only be called a once -/// as soon as we know that the staff mode is enabled. -pub fn staff_mode(cx: &mut AppContext, mut init: F) { - if **cx.default_global::() { - init(cx) - } else { - let mut once = Some(()); - cx.observe_global::(move |cx| { - if **cx.global::() && once.take().is_some() { - init(cx); - } - }) - .detach(); - } -} - -/// Immediately checks and runs the init function if the staff mode is not enabled. -/// This is only included for symettry with staff_mode() above -pub fn not_staff_mode(cx: &mut AppContext, init: F) { - if !**cx.default_global::() { - init(cx) - } -} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 377f64aad6f1579dfe9ebb50fb0e8b9c683e0f01..7e97d3918606e42cbadd62e354bea5ded0f44e76 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -16,7 +16,7 @@ gpui = { path = "../gpui" } picker = { path = "../picker" } theme = { path = "../theme" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } workspace = { path = "../workspace" } util = { path = "../util" } log.workspace = true diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 551000573300a16334a6a44035c91e8777af14d2..1969b0256a3aa5ee9c203ee02b765695bb748bf9 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,9 +1,9 @@ +use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext}; use picker::{Picker, PickerDelegate, PickerEvent}; use settings::{update_settings_file, SettingsStore}; -use staff_mode::StaffMode; use std::sync::Arc; use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use util::ResultExt; @@ -54,7 +54,7 @@ impl ThemeSelectorDelegate { fn new(fs: Arc, cx: &mut ViewContext) -> Self { let original_theme = theme::current(cx).clone(); - let staff_mode = **cx.default_global::(); + let staff_mode = cx.is_staff(); let registry = cx.global::>(); let mut theme_names = registry.list(staff_mode).collect::>(); theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 92900f84cb54ea9563de2618989bc4aac470f417..2a977646470507565dbea2b5ac847d2546f16845 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -60,7 +60,7 @@ quick_action_bar = { path = "../quick_action_bar" } recent_projects = { path = "../recent_projects" } rpc = { path = "../rpc" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } terminal_view = { path = "../terminal_view" } diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index b7e4ab4ba7b32491bbbb8aa025cab543dde113af..61d19ce5b6546ce1de8f040fc97c70b322730f3b 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; +use feature_flags::FeatureFlagAppExt; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; @@ -9,7 +10,6 @@ use node_runtime::NodeRuntime; use serde_json::json; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::fs; -use staff_mode::StaffMode; use std::{ any::Any, ffi::OsString, @@ -104,7 +104,7 @@ impl LspAdapter for JsonLspAdapter { cx: &mut AppContext, ) -> Option> { let action_names = cx.all_action_names().collect::>(); - let staff_mode = cx.default_global::().0; + let staff_mode = cx.is_staff(); let language_names = &self.languages.language_names(); let settings_schema = cx.global::().json_schema( &SettingsJsonSchemaParams { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3b1fccb927b9397b723006a5f868cd6aa37bff50..da726eef65d16e9ffeaa84141506c7e1b184a76a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -53,8 +53,6 @@ use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; -#[cfg(debug_assertions)] -use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::AppState; use zed::{ @@ -122,7 +120,10 @@ fn main() { cx.set_global(*RELEASE_CHANNEL); #[cfg(debug_assertions)] - cx.set_global(StaffMode(true)); + { + use feature_flags::FeatureFlagAppExt; + cx.set_staff(true); + } let mut store = SettingsStore::default(); store From 2b007930a9ed71c5534e771096ecaa10294d9edd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 03:52:52 +0300 Subject: [PATCH 105/142] Remove query ranges for failed inlay hint requests --- crates/editor/src/inlay_hint_cache.rs | 122 +++++++++++++++++++++----- 1 file changed, 100 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 0067c832d36bf87758563774206186411d94075a..1526e16129466a42631883aa5154f5e3117a7039 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -108,17 +108,23 @@ impl TasksForRanges { updated_ranges.before_visible = updated_ranges .before_visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges.visible = updated_ranges .visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges.after_visible = updated_ranges .after_visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges } @@ -134,7 +140,7 @@ impl TasksForRanges { } } - fn remove_cached_ranges( + fn remove_cached_ranges_from_query( &mut self, buffer_snapshot: &BufferSnapshot, query_range: Range, @@ -196,6 +202,52 @@ impl TasksForRanges { ranges_to_query } + + fn remove_from_cached_ranges( + &mut self, + buffer: &BufferSnapshot, + range_to_remove: Range, + ) { + self.sorted_ranges = self + .sorted_ranges + .drain(..) + .filter_map(|mut cached_range| { + if cached_range.start.cmp(&range_to_remove.end, buffer).is_gt() + || cached_range.end.cmp(&range_to_remove.start, buffer).is_lt() + { + Some(vec![cached_range]) + } else if cached_range + .start + .cmp(&range_to_remove.start, buffer) + .is_ge() + && cached_range.end.cmp(&range_to_remove.end, buffer).is_le() + { + None + } else if range_to_remove + .start + .cmp(&cached_range.start, buffer) + .is_ge() + && range_to_remove.end.cmp(&cached_range.end, buffer).is_le() + { + Some(vec![ + cached_range.start..range_to_remove.start, + range_to_remove.end..cached_range.end, + ]) + } else if cached_range + .start + .cmp(&range_to_remove.start, buffer) + .is_ge() + { + cached_range.start = range_to_remove.end; + Some(vec![cached_range]) + } else { + cached_range.end = range_to_remove.start; + Some(vec![cached_range]) + } + }) + .flatten() + .collect(); + } } impl InlayHintCache { @@ -692,7 +744,8 @@ fn new_update_task( cached_excerpt_hints: Option>>, cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { - cx.spawn(|editor, cx| async move { + cx.spawn(|editor, mut cx| async move { + let closure_cx = cx.clone(); let fetch_and_update_hints = |invalidate, range| { fetch_and_update_hints( editor.clone(), @@ -703,37 +756,62 @@ fn new_update_task( query, invalidate, range, - cx.clone(), + closure_cx.clone(), ) }; - let visible_range_update_results = - future::join_all(query_ranges.visible.into_iter().map(|visible_range| { - fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) - })) - .await; - for result in visible_range_update_results { + let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( + |visible_range| async move { + ( + visible_range.clone(), + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + .await, + ) + }, + )) + .await; + + let hint_delay = cx.background().timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )); + + let mut query_range_failed = |range: Range, e: anyhow::Error| { + error!("inlay hint update task for range {range:?} failed: {e:#}"); + editor + .update(&mut cx, |editor, _| { + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.remove_from_cached_ranges(&buffer_snapshot, range); + } + }) + .ok() + }; + + for (range, result) in visible_range_update_results { if let Err(e) = result { - error!("visible range inlay hint update task failed: {e:#}"); + query_range_failed(range, e); } } - cx.background() - .timer(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, - )) - .await; - + hint_delay.await; let invisible_range_update_results = future::join_all( query_ranges .before_visible .into_iter() .chain(query_ranges.after_visible.into_iter()) - .map(|invisible_range| fetch_and_update_hints(false, invisible_range)), + .map(|invisible_range| async move { + ( + invisible_range.clone(), + fetch_and_update_hints(false, invisible_range).await, + ) + }), ) .await; - for result in invisible_range_update_results { + for (range, result) in invisible_range_update_results { if let Err(e) = result { - error!("invisible range inlay hint update task failed: {e:#}"); + query_range_failed(range, e); } } }) From e6fb909d8990ee43f4f3b4381e0d3d8b4615bc7e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 13:06:50 +0300 Subject: [PATCH 106/142] Limit LSP non-invalidating queries --- crates/editor/src/inlay_hint_cache.rs | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1526e16129466a42631883aa5154f5e3117a7039..4e040a8a7e0aff8dd8d4280f1f0b90ec4786736c 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -19,6 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use smol::lock::Semaphore; use sum_tree::Bias; use text::ToOffset; use util::post_inc; @@ -29,6 +30,7 @@ pub struct InlayHintCache { version: usize, pub(super) enabled: bool, update_tasks: HashMap, + lsp_request_limiter: Arc, } #[derive(Debug)] @@ -258,6 +260,7 @@ impl InlayHintCache { hints: HashMap::default(), update_tasks: HashMap::default(), version: 0, + lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), } } @@ -629,6 +632,7 @@ fn spawn_new_update_tasks( buffer_snapshot.clone(), Arc::clone(&visible_hints), cached_excerpt_hints, + Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), cx, ) }; @@ -733,6 +737,7 @@ fn determine_query_ranges( }) } +const MAX_CONCURRENT_LSP_REQUESTS: usize = 5; const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; fn new_update_task( @@ -742,6 +747,7 @@ fn new_update_task( buffer_snapshot: BufferSnapshot, visible_hints: Arc>, cached_excerpt_hints: Option>>, + lsp_request_limiter: Arc, cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { cx.spawn(|editor, mut cx| async move { @@ -756,6 +762,7 @@ fn new_update_task( query, invalidate, range, + Arc::clone(&lsp_request_limiter), closure_cx.clone(), ) }; @@ -826,10 +833,41 @@ async fn fetch_and_update_hints( query: ExcerptQuery, invalidate: bool, fetch_range: Range, + lsp_request_limiter: Arc, mut cx: gpui::AsyncAppContext, ) -> anyhow::Result<()> { + let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { + (None, false) + } else { + match lsp_request_limiter.try_acquire() { + Some(guard) => (Some(guard), false), + None => (Some(lsp_request_limiter.acquire().await), true), + } + }; let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { + if got_throttled { + if let Some((_, _, current_visible_range)) = editor + .excerpt_visible_offsets(None, cx) + .remove(&query.excerpt_id) + { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + if !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + { + return None; + } + } + } editor .buffer() .read(cx) @@ -847,6 +885,8 @@ async fn fetch_and_update_hints( Some(task) => task.await.context("inlay hint fetch task")?, None => return Ok(()), }; + drop(lsp_request_guard); + let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx From 3fc48fc2774785b8bbc96a04646d1b079cf9a9da Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 13:46:55 +0300 Subject: [PATCH 107/142] Log LSP inlay hint path --- crates/editor/src/editor.rs | 13 ++++++++ crates/editor/src/inlay_hint_cache.rs | 44 ++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 79186d6e8ca7ec9ff47a74552d45582951f55752..8f0c8c9f220d9fa63bb491174c28508d49a8b23f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1252,6 +1252,17 @@ enum InlayHintRefreshReason { BufferEdited(HashSet>), RefreshRequested, } +impl InlayHintRefreshReason { + fn description(&self) -> &'static str { + match self { + InlayHintRefreshReason::Toggle(_) => "toggle", + InlayHintRefreshReason::SettingsChange(_) => "settings change", + InlayHintRefreshReason::NewLinesShown => "new lines shown", + InlayHintRefreshReason::BufferEdited(_) => "buffer edited", + InlayHintRefreshReason::RefreshRequested => "refresh requested", + } + } +} impl Editor { pub fn single_line( @@ -2741,6 +2752,7 @@ impl Editor { return; } + let reason_description = reason.description(); let (invalidate_cache, required_languages) = match reason { InlayHintRefreshReason::Toggle(enabled) => { self.inlay_hint_cache.enabled = enabled; @@ -2790,6 +2802,7 @@ impl Editor { to_remove, to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( + reason_description, self.excerpt_visible_offsets(required_languages.as_ref(), cx), invalidate_cache, cx, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 4e040a8a7e0aff8dd8d4280f1f0b90ec4786736c..e16971fbbe4feda8669f4be2e14f3908d45932a8 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -13,7 +13,6 @@ use clock::Global; use futures::future; use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; -use log::error; use parking_lot::RwLock; use project::{InlayHint, ResolveState}; @@ -21,7 +20,7 @@ use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; use smol::lock::Semaphore; use sum_tree::Bias; -use text::ToOffset; +use text::{ToOffset, ToPoint}; use util::post_inc; pub struct InlayHintCache { @@ -74,6 +73,7 @@ struct ExcerptQuery { excerpt_id: ExcerptId, cache_version: usize, invalidate: InvalidationStrategy, + reason: &'static str, } impl InvalidationStrategy { @@ -317,6 +317,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, + reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, @@ -345,7 +346,14 @@ impl InlayHintCache { cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { - spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx) + spawn_new_update_tasks( + editor, + reason, + excerpts_to_query, + invalidate, + cache_version, + cx, + ) }) .ok(); }) @@ -568,6 +576,7 @@ impl InlayHintCache { fn spawn_new_update_tasks( editor: &mut Editor, + reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, update_cache_version: usize, @@ -622,6 +631,7 @@ fn spawn_new_update_tasks( excerpt_id, cache_version: update_cache_version, invalidate, + reason, }; let new_update_task = |query_ranges| { @@ -782,7 +792,7 @@ fn new_update_task( )); let mut query_range_failed = |range: Range, e: anyhow::Error| { - error!("inlay hint update task for range {range:?} failed: {e:#}"); + log::error!("inlay hint update task for range {range:?} failed: {e:#}"); editor .update(&mut cx, |editor, _| { if let Some(task_ranges) = editor @@ -844,6 +854,8 @@ async fn fetch_and_update_hints( None => (Some(lsp_request_limiter.acquire().await), true), } }; + let fetch_range_to_log = + fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { @@ -882,10 +894,25 @@ async fn fetch_and_update_hints( .ok() .flatten(); let new_hints = match inlay_hints_fetch_task { - Some(task) => task.await.context("inlay hint fetch task")?, + Some(fetch_task) => { + log::debug!( + "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", + query_reason = query.reason, + ); + log::trace!( + "Currently visible hints: {visible_hints:?}, cached hints present: {}", + cached_excerpt_hints.is_some(), + ); + fetch_task.await.context("inlay hint fetch task")? + } None => return Ok(()), }; drop(lsp_request_guard); + log::debug!( + "Fetched {} hints for range {fetch_range_to_log:?}", + new_hints.len() + ); + log::trace!("Fetched hints: {new_hints:?}"); let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); @@ -904,6 +931,13 @@ async fn fetch_and_update_hints( }) .await; if let Some(new_update) = new_update { + log::info!( + "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", + new_update.remove_from_visible.len(), + new_update.remove_from_cache.len(), + new_update.add_to_cache.len() + ); + log::trace!("New update: {new_update:?}"); editor .update(&mut cx, |editor, cx| { apply_hint_update( From 48659d3b3cdd997d3e94ebcb2c4e77e2b6b86211 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:13:21 +0300 Subject: [PATCH 108/142] Treat multibuffer edit events properly Miltibuffer emits edit events even if it only got an excerpt added/removed/etc. Separate buffer edits and trigger hint invalidation refresh for them only, also trigger hint new lines refresh on excerpt addition events. --- crates/editor/src/editor.rs | 67 +++++++++++++++++-------------- crates/editor/src/multi_buffer.rs | 36 +++++++++++++---- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8f0c8c9f220d9fa63bb491174c28508d49a8b23f..5e9d604de5c64737bb37052c88868c4a8eb4442c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7896,7 +7896,9 @@ impl Editor { cx: &mut ViewContext, ) { match event { - multi_buffer::Event::Edited => { + multi_buffer::Event::Edited { + sigleton_buffer_edited, + } => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); if self.has_active_copilot_suggestion(cx) { @@ -7904,30 +7906,32 @@ impl Editor { } cx.emit(Event::BufferEdited); - if let Some(project) = &self.project { - let project = project.read(cx); - let languages_affected = multibuffer - .read(cx) - .all_buffers() - .into_iter() - .filter_map(|buffer| { - let buffer = buffer.read(cx); - let language = buffer.language()?; - if project.is_local() - && project.language_servers_for_buffer(buffer, cx).count() == 0 - { - None - } else { - Some(language) - } - }) - .cloned() - .collect::>(); - if !languages_affected.is_empty() { - self.refresh_inlay_hints( - InlayHintRefreshReason::BufferEdited(languages_affected), - cx, - ); + if *sigleton_buffer_edited { + if let Some(project) = &self.project { + let project = project.read(cx); + let languages_affected = multibuffer + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + let language = buffer.language()?; + if project.is_local() + && project.language_servers_for_buffer(buffer, cx).count() == 0 + { + None + } else { + Some(language) + } + }) + .cloned() + .collect::>(); + if !languages_affected.is_empty() { + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(languages_affected), + cx, + ); + } } } } @@ -7935,11 +7939,14 @@ impl Editor { buffer, predecessor, excerpts, - } => cx.emit(Event::ExcerptsAdded { - buffer: buffer.clone(), - predecessor: *predecessor, - excerpts: excerpts.clone(), - }), + } => { + cx.emit(Event::ExcerptsAdded { + buffer: buffer.clone(), + predecessor: *predecessor, + excerpts: excerpts.clone(), + }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } multi_buffer::Event::ExcerptsRemoved { ids } => { cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 5c0d8b641cac5731508beae589a499727aac0dd8..e84cfb85aa0ce0a1d0525b1b40268923852f6d4f 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -67,7 +67,9 @@ pub enum Event { ExcerptsEdited { ids: Vec, }, - Edited, + Edited { + sigleton_buffer_edited: bool, + }, Reloaded, DiffBaseChanged, LanguageChanged, @@ -1022,7 +1024,9 @@ impl MultiBuffer { old: edit_start..edit_start, new: edit_start..edit_end, }]); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsAdded { buffer, predecessor: prev_excerpt_id, @@ -1046,7 +1050,9 @@ impl MultiBuffer { old: 0..prev_len, new: 0..0, }]); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1254,7 +1260,9 @@ impl MultiBuffer { } self.subscriptions.publish_mut(edits); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1315,7 +1323,9 @@ impl MultiBuffer { cx: &mut ModelContext, ) { cx.emit(match event { - language::Event::Edited => Event::Edited, + language::Event::Edited => Event::Edited { + sigleton_buffer_edited: true, + }, language::Event::DirtyChanged => Event::DirtyChanged, language::Event::Saved => Event::Saved, language::Event::FileHandleChanged => Event::FileHandleChanged, @@ -4078,7 +4088,7 @@ mod tests { multibuffer.update(cx, |_, cx| { let events = events.clone(); cx.subscribe(&multibuffer, move |_, _, event, _| { - if let Event::Edited = event { + if let Event::Edited { .. } = event { events.borrow_mut().push(event.clone()) } }) @@ -4133,7 +4143,17 @@ mod tests { // Adding excerpts emits an edited event. assert_eq!( events.borrow().as_slice(), - &[Event::Edited, Event::Edited, Event::Edited] + &[ + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + } + ] ); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -4312,7 +4332,7 @@ mod tests { excerpts, } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), - Event::Edited => { + Event::Edited { .. } => { *follower_edit_event_count.borrow_mut() += 1; } _ => {} From 9bdf76f4453739d9593e2b50e6f0614f7affadc1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:42:20 +0300 Subject: [PATCH 109/142] Properly handle hover-less areas hover --- crates/editor/src/link_go_to_definition.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 280993dd26499cf529ef62e905c140bcc35982bc..84fd9b5cc1ee44d2cd5cf22153e2ce0b9d27af61 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -173,6 +173,8 @@ pub fn update_inlay_link_and_hover_points( } else { None }; + let mut go_to_definition_updated = false; + let mut hover_updated = false; if let Some(hovered_offset) = hovered_offset { let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); let previous_valid_anchor = buffer_snapshot.anchor_at( @@ -183,9 +185,6 @@ pub fn update_inlay_link_and_hover_points( point_for_position.next_valid.to_point(snapshot), Bias::Right, ); - - let mut go_to_definition_updated = false; - let mut hover_updated = false; if let Some(hovered_hint) = editor .visible_inlay_hints(cx) .into_iter() @@ -324,13 +323,13 @@ pub fn update_inlay_link_and_hover_points( } } } + } - if !go_to_definition_updated { - update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); - } - if !hover_updated { - hover_popover::hover_at(editor, None, cx); - } + if !go_to_definition_updated { + update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); } } From 2a42a08f46116d402286388571403339153c1b63 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:47:38 +0300 Subject: [PATCH 110/142] Invalidate skipped throttled hint fetch tasks' ranges --- crates/editor/src/inlay_hint_cache.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index e16971fbbe4feda8669f4be2e14f3908d45932a8..ff0d0082ae01df2c49901a35dbd52793bb48ddda 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -208,7 +208,7 @@ impl TasksForRanges { fn remove_from_cached_ranges( &mut self, buffer: &BufferSnapshot, - range_to_remove: Range, + range_to_remove: &Range, ) { self.sorted_ranges = self .sorted_ranges @@ -791,7 +791,7 @@ fn new_update_task( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, )); - let mut query_range_failed = |range: Range, e: anyhow::Error| { + let mut query_range_failed = |range: &Range, e: anyhow::Error| { log::error!("inlay hint update task for range {range:?} failed: {e:#}"); editor .update(&mut cx, |editor, _| { @@ -800,7 +800,7 @@ fn new_update_task( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, range); + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &range); } }) .ok() @@ -808,7 +808,7 @@ fn new_update_task( for (range, result) in visible_range_update_results { if let Err(e) = result { - query_range_failed(range, e); + query_range_failed(&range, e); } } @@ -828,7 +828,7 @@ fn new_update_task( .await; for (range, result) in invisible_range_update_results { if let Err(e) = result { - query_range_failed(range, e); + query_range_failed(&range, e); } } }) @@ -876,6 +876,14 @@ async fn fetch_and_update_hints( && !double_visible_range .contains(&fetch_range.end.to_offset(&buffer_snapshot)) { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); + } return None; } } From 84284099e201532c2e6f51be2efc5a1dce3ca255 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 15:04:48 +0300 Subject: [PATCH 111/142] Properly handle padding when highlighting inlay hints --- crates/editor/src/hover_popover.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 89e8d8e246cfe7dee57fb51ec16ab817bf88d69f..f5f663660daa04ef20bb042310ccd30cd75b6a7b 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -65,8 +65,7 @@ pub fn find_hovered_hint_part( if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; let mut part_start = hint_range.start; - let last_label_part_index = label_parts.len() - 1; - for (i, part) in label_parts.into_iter().enumerate() { + for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; @@ -77,8 +76,9 @@ pub fn find_hovered_hint_part( part_start.0 += 1; part_end.0 += 1; } - if padding_right && i == last_label_part_index { - part_end.0 -= 1; + if padding_right { + part_start.0 += 1; + part_end.0 += 1; } return Some((part, part_start..part_end)); } From f8a8b998cebbd486e94fc5b7c95d83fe36d05c96 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 15:21:45 +0300 Subject: [PATCH 112/142] Properly react on excerpts drop --- crates/editor/src/editor.rs | 21 ++++++++++++++++----- crates/editor/src/inlay_hint_cache.rs | 22 ++++++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5e9d604de5c64737bb37052c88868c4a8eb4442c..01aa59574dc9455c2f5caee8f53144b1ba021446 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1251,15 +1251,17 @@ enum InlayHintRefreshReason { NewLinesShown, BufferEdited(HashSet>), RefreshRequested, + ExcerptsRemoved(Vec), } impl InlayHintRefreshReason { fn description(&self) -> &'static str { match self { - InlayHintRefreshReason::Toggle(_) => "toggle", - InlayHintRefreshReason::SettingsChange(_) => "settings change", - InlayHintRefreshReason::NewLinesShown => "new lines shown", - InlayHintRefreshReason::BufferEdited(_) => "buffer edited", - InlayHintRefreshReason::RefreshRequested => "refresh requested", + Self::Toggle(_) => "toggle", + Self::SettingsChange(_) => "settings change", + Self::NewLinesShown => "new lines shown", + Self::BufferEdited(_) => "buffer edited", + Self::RefreshRequested => "refresh requested", + Self::ExcerptsRemoved(_) => "excerpts removed", } } } @@ -2789,6 +2791,14 @@ impl Editor { ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), } } + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + let InlaySplice { + to_remove, + to_insert, + } = self.inlay_hint_cache.remove_excerpts(excerpts_removed); + self.splice_inlay_hints(to_remove, to_insert, cx); + return; + } InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), InlayHintRefreshReason::BufferEdited(buffer_languages) => { (InvalidationStrategy::BufferEdited, Some(buffer_languages)) @@ -7948,6 +7958,7 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } multi_buffer::Event::ExcerptsRemoved { ids } => { + self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) } multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed), diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index ff0d0082ae01df2c49901a35dbd52793bb48ddda..1ed35ef629688575dbf25d0c64fecdd9f3489fa2 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -474,6 +474,24 @@ impl InlayHintCache { } } + pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> InlaySplice { + let mut to_remove = Vec::new(); + for excerpt_to_remove in excerpts_removed { + self.update_tasks.remove(&excerpt_to_remove); + if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) { + let cached_hints = cached_hints.read(); + to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id)); + } + } + if !to_remove.is_empty() { + self.version += 1; + } + InlaySplice { + to_remove, + to_insert: Vec::new(), + } + } + pub fn clear(&mut self) { self.version += 1; self.update_tasks.clear(); @@ -2956,7 +2974,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 2, + 3, "Excerpt removal should trigger a cache update" ); }); @@ -2984,7 +3002,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 3, + 4, "Settings change should trigger a cache update" ); }); From 73937876b6fb3fca890805a941b203ef1adfe7dd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 21:12:04 +0300 Subject: [PATCH 113/142] Properly omit throttled hint queries --- crates/editor/src/inlay_hint_cache.rs | 50 +++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1ed35ef629688575dbf25d0c64fecdd9f3489fa2..2adf3caaf1c1b2782dc5187223847a4b6301c878 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -877,33 +877,33 @@ async fn fetch_and_update_hints( let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { - if let Some((_, _, current_visible_range)) = editor - .excerpt_visible_offsets(None, cx) - .remove(&query.excerpt_id) - { - let visible_offset_length = current_visible_range.len(); - let double_visible_range = current_visible_range - .start - .saturating_sub(visible_offset_length) - ..current_visible_range - .end - .saturating_add(visible_offset_length) - .min(buffer_snapshot.len()); - if !double_visible_range - .contains(&fetch_range.start.to_offset(&buffer_snapshot)) - && !double_visible_range - .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + Some((_, _, current_visible_range)) => { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + }, + None => true, + }; + if query_not_around_visible_range { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) { - log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); - if let Some(task_ranges) = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) - { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); - } - return None; + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); } + return None; } } editor From 5cf51211b64b0c70e442402939d08e5bee4e17c7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 21:37:34 +0300 Subject: [PATCH 114/142] Use better names, simplify --- crates/editor/src/editor.rs | 8 +- crates/editor/src/inlay_hint_cache.rs | 119 ++++++++++++-------------- 2 files changed, 58 insertions(+), 69 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 01aa59574dc9455c2f5caee8f53144b1ba021446..2d934a1dc8f213d35648e3fd5799ccf8c08321c3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2792,11 +2792,13 @@ impl Editor { } } InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { - let InlaySplice { + if let Some(InlaySplice { to_remove, to_insert, - } = self.inlay_hint_cache.remove_excerpts(excerpts_removed); - self.splice_inlay_hints(to_remove, to_insert, cx); + }) = self.inlay_hint_cache.remove_excerpts(excerpts_removed) + { + self.splice_inlay_hints(to_remove, to_insert, cx); + } return; } InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 2adf3caaf1c1b2782dc5187223847a4b6301c878..34898aea2efe7ec45229cdb4e63de13cd48217f9 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -104,37 +104,34 @@ impl TasksForRanges { invalidate: InvalidationStrategy, spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { - let query_ranges = match invalidate { - InvalidationStrategy::None => { - let mut updated_ranges = query_ranges; - updated_ranges.before_visible = updated_ranges - .before_visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges.visible = updated_ranges - .visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges.after_visible = updated_ranges - .after_visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges - } - InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { - self.tasks.clear(); - self.sorted_ranges.clear(); - query_ranges - } + let query_ranges = if invalidate.should_invalidate() { + self.tasks.clear(); + self.sorted_ranges.clear(); + query_ranges + } else { + let mut non_cached_query_ranges = query_ranges; + non_cached_query_ranges.before_visible = non_cached_query_ranges + .before_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.visible = non_cached_query_ranges + .visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.after_visible = non_cached_query_ranges + .after_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges }; if !query_ranges.is_empty() { @@ -205,45 +202,31 @@ impl TasksForRanges { ranges_to_query } - fn remove_from_cached_ranges( - &mut self, - buffer: &BufferSnapshot, - range_to_remove: &Range, - ) { + fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range) { self.sorted_ranges = self .sorted_ranges .drain(..) .filter_map(|mut cached_range| { - if cached_range.start.cmp(&range_to_remove.end, buffer).is_gt() - || cached_range.end.cmp(&range_to_remove.start, buffer).is_lt() + if cached_range.start.cmp(&range.end, buffer).is_gt() + || cached_range.end.cmp(&range.start, buffer).is_lt() { Some(vec![cached_range]) - } else if cached_range - .start - .cmp(&range_to_remove.start, buffer) - .is_ge() - && cached_range.end.cmp(&range_to_remove.end, buffer).is_le() + } else if cached_range.start.cmp(&range.start, buffer).is_ge() + && cached_range.end.cmp(&range.end, buffer).is_le() { None - } else if range_to_remove - .start - .cmp(&cached_range.start, buffer) - .is_ge() - && range_to_remove.end.cmp(&cached_range.end, buffer).is_le() + } else if range.start.cmp(&cached_range.start, buffer).is_ge() + && range.end.cmp(&cached_range.end, buffer).is_le() { Some(vec![ - cached_range.start..range_to_remove.start, - range_to_remove.end..cached_range.end, + cached_range.start..range.start, + range.end..cached_range.end, ]) - } else if cached_range - .start - .cmp(&range_to_remove.start, buffer) - .is_ge() - { - cached_range.start = range_to_remove.end; + } else if cached_range.start.cmp(&range.start, buffer).is_ge() { + cached_range.start = range.end; Some(vec![cached_range]) } else { - cached_range.end = range_to_remove.start; + cached_range.end = range.start; Some(vec![cached_range]) } }) @@ -474,7 +457,7 @@ impl InlayHintCache { } } - pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> InlaySplice { + pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> Option { let mut to_remove = Vec::new(); for excerpt_to_remove in excerpts_removed { self.update_tasks.remove(&excerpt_to_remove); @@ -483,17 +466,21 @@ impl InlayHintCache { to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id)); } } - if !to_remove.is_empty() { + if to_remove.is_empty() { + None + } else { self.version += 1; - } - InlaySplice { - to_remove, - to_insert: Vec::new(), + Some(InlaySplice { + to_remove, + to_insert: Vec::new(), + }) } } pub fn clear(&mut self) { - self.version += 1; + if !self.update_tasks.is_empty() || !self.hints.is_empty() { + self.version += 1; + } self.update_tasks.clear(); self.hints.clear(); } @@ -818,7 +805,7 @@ fn new_update_task( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &range); + task_ranges.invalidate_range(&buffer_snapshot, &range); } }) .ok() @@ -901,7 +888,7 @@ async fn fetch_and_update_hints( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); + task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); } return None; } From dad64edde107d7d6bd7d8f835c299ddf1d43d5df Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 15:14:45 +0300 Subject: [PATCH 115/142] Better highlight hint ranges --- crates/editor/src/hover_popover.rs | 14 ++-------- crates/editor/src/link_go_to_definition.rs | 32 ++++++++++------------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index f5f663660daa04ef20bb042310ccd30cd75b6a7b..4eda65fc122b947fe44e7325a022ca6885668ccd 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -57,8 +57,6 @@ pub struct InlayHover { pub fn find_hovered_hint_part( label_parts: Vec, - padding_left: bool, - padding_right: bool, hint_range: Range, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { @@ -67,19 +65,11 @@ pub fn find_hovered_hint_part( let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); - if hovered_character >= part_len { + if hovered_character > part_len { hovered_character -= part_len; part_start.0 += part_len; } else { - let mut part_end = InlayOffset(part_start.0 + part_len); - if padding_left { - part_start.0 += 1; - part_end.0 += 1; - } - if padding_right { - part_start.0 += 1; - part_end.0 += 1; - } + let part_end = InlayOffset(part_start.0 + part_len); return Some((part, part_start..part_end)); } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 84fd9b5cc1ee44d2cd5cf22153e2ce0b9d27af61..a36c673eae74ab817ff17ffe49b0288ffc48a681 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -165,11 +165,8 @@ pub fn update_inlay_link_and_hover_points( snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); let hint_end_offset = snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { + let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 { Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) } else { None }; @@ -215,17 +212,18 @@ pub fn update_inlay_link_and_hover_points( } } ResolveState::Resolved => { + let mut actual_hint_start = hint_start_offset; + let mut actual_hint_end = hint_end_offset; + if cached_hint.padding_left { + actual_hint_start.0 += 1; + actual_hint_end.0 += 1; + } + if cached_hint.padding_right { + actual_hint_start.0 += 1; + actual_hint_end.0 += 1; + } match cached_hint.label { project::InlayHintLabel::String(_) => { - let mut highlight_start = hint_start_offset; - let mut highlight_end = hint_end_offset; - if cached_hint.padding_left { - highlight_start.0 += 1; - highlight_end.0 += 1; - } - if cached_hint.padding_right { - highlight_end.0 -= 1; - } if let Some(tooltip) = cached_hint.tooltip { hover_popover::hover_at_inlay( editor, @@ -246,8 +244,8 @@ pub fn update_inlay_link_and_hover_points( triggered_from: hovered_offset, range: InlayRange { inlay_position: hovered_hint.position, - highlight_start, - highlight_end, + highlight_start: actual_hint_start, + highlight_end: actual_hint_end, }, }, cx, @@ -259,9 +257,7 @@ pub fn update_inlay_link_and_hover_points( if let Some((hovered_hint_part, part_range)) = hover_popover::find_hovered_hint_part( label_parts, - cached_hint.padding_left, - cached_hint.padding_right, - hint_start_offset..hint_end_offset, + actual_hint_start..actual_hint_end, hovered_offset, ) { From 693e91f3351d64e85f9f4fe7de466a109445d139 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 18:23:40 +0300 Subject: [PATCH 116/142] Properly compare previous hover trigger point when hover changes --- crates/editor/src/link_go_to_definition.rs | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index a36c673eae74ab817ff17ffe49b0288ffc48a681..909c07880b749ca93d37abea83d6b5bc68f1fa38 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -23,6 +23,7 @@ pub struct LinkGoToDefinitionState { pub task: Option>>, } +#[derive(Debug)] pub enum GoToDefinitionTrigger { Text(DisplayPoint), InlayHint(InlayRange, lsp::Location, LanguageServerId), @@ -81,7 +82,7 @@ impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(coordinates, _, _) => &coordinates.inlay_position, + TriggerPoint::InlayHint(range, _, _) => &range.inlay_position, } } @@ -127,11 +128,22 @@ pub fn update_go_to_definition_link( &trigger_point, &editor.link_go_to_definition_state.last_trigger_point, ) { - if a.anchor() - .cmp(b.anchor(), &snapshot.buffer_snapshot) - .is_eq() - { - return; + match (a, b) { + (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => { + if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() { + return; + } + } + (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => { + if range_a + .inlay_position + .cmp(&range_b.inlay_position, &snapshot.buffer_snapshot) + .is_eq() + { + return; + } + } + _ => {} } } From 81e70905bb20ded519ed987c0948289a541a3a19 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 19:12:32 +0300 Subject: [PATCH 117/142] Do not allow cmd+click in invalid inlay context --- crates/editor/src/element.rs | 2 +- crates/editor/src/link_go_to_definition.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 684f92a96a8ee8b11e218960d6ac556d75187bfe..62f4c8c8065e8eb24ef24e7b1c98e75168034f43 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2755,7 +2755,7 @@ impl PointForPosition { } } - fn as_valid(&self) -> Option { + pub fn as_valid(&self) -> Option { if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { Some(self.previous_valid) } else { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 909c07880b749ca93d37abea83d6b5bc68f1fa38..9ca39f9b307769ffa818f29d55c063e974c4213e 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -596,9 +596,11 @@ fn go_to_fetched_definition_of_kind( cx, ); - match kind { - LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), - LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + if point.as_valid().is_some() { + match kind { + LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), + LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + } } } } From 38da2a587a74eeb37e6a715a37ea03df82a430e2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 19:41:15 +0300 Subject: [PATCH 118/142] Fix the tests --- crates/editor/src/hover_popover.rs | 40 +++++++++++++++------- crates/editor/src/link_go_to_definition.rs | 16 ++++++--- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 4eda65fc122b947fe44e7325a022ca6885668ccd..2f278ce262f6dd3e0910b022e6956f26c9bdfa6b 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1376,13 +1376,21 @@ mod tests { .unwrap(); let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) + as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (entire_hint_label.find(new_type_label).unwrap() - + new_type_label.len() / 2) - as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); cx.update_editor(|editor, cx| { @@ -1504,13 +1512,21 @@ mod tests { let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) + as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (entire_hint_label.find(struct_label).unwrap() - + struct_label.len() / 2) - as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); cx.update_editor(|editor, cx| { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 9ca39f9b307769ffa818f29d55c063e974c4213e..1f9a3aab730d4d6077df836e54ba09bc12f397d1 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1170,11 +1170,19 @@ mod tests { .unwrap(); let hint_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + (hint_label.len() / 2) as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (hint_label.len() / 2) as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); // Press cmd to trigger highlight From 3bfe78b1dfb0b991d16de1a0be4ac9ec46b741f8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 28 Aug 2023 00:27:59 +0300 Subject: [PATCH 119/142] Use proper property names for inlay hint resolve capabilities --- crates/lsp/src/lsp.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 7cba03955280dbed1cd98fd9a7b61a1b526c440f..d49dafff2f99fd1c132c01349a363096cd63183a 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -435,7 +435,13 @@ impl LanguageServer { }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { - properties: vec!["textEdits".to_string(), "tooltip".to_string()], + properties: vec![ + "textEdits".to_string(), + "tooltip".to_string(), + "label.tooltip".to_string(), + "label.location".to_string(), + "label.command".to_string(), + ], }), dynamic_registration: Some(false), }), From 506ec01df3112b9d392ff56cb716d8e831306af9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 28 Aug 2023 11:19:57 +0300 Subject: [PATCH 120/142] Allow `[` and `]` symbols in terminal links ` ./src/pages/[[...slug]].tsx` is a valid file path in macOs and Linux, and should be available for cmd-hover-click in terminal. --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e28e0ca5c16bb15454aa2d9bbc248963df56c2a9..83ba056485664057a13c731033ccf1557a332fc8 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -78,7 +78,7 @@ lazy_static! { // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings. static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap(); - static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.:/@\-~]+"#).unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap(); } ///Upward flowing events, for changing the title and such From 07b9c6c302d89b3e23f61e9943b2c6b3ab113d99 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:51:50 +0200 Subject: [PATCH 121/142] language: Make Buffer::new take an explicit ID (#2900) See Linear description for the full explanation of the issue. This PR is mostly a mechanical change, except for the one case where we do pass in an explicit `next_id` instead of `model_id` in project.rs. Release Notes: - Fixed a bug where some results were not reported in project search in presence of unnamed buffers. --- crates/ai/src/assistant.rs | 8 +-- crates/copilot/src/copilot.rs | 4 +- crates/editor/src/display_map.rs | 9 ++- crates/editor/src/editor.rs | 6 +- crates/editor/src/editor_tests.rs | 41 +++++++----- crates/editor/src/movement.rs | 3 +- crates/editor/src/multi_buffer.rs | 47 ++++++++------ crates/language/src/buffer.rs | 8 +-- crates/language/src/buffer_tests.rs | 90 +++++++++++++++----------- crates/language_tools/src/lsp_log.rs | 8 ++- crates/project/src/project.rs | 4 +- crates/search/src/buffer_search.rs | 6 +- crates/zed/src/languages/c.rs | 2 +- crates/zed/src/languages/python.rs | 2 +- crates/zed/src/languages/rust.rs | 2 +- crates/zed/src/languages/typescript.rs | 5 +- 16 files changed, 141 insertions(+), 104 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 2699bf40a088b38033af9244c4365d2c57d43dbf..3c561b0e03944c5ac1c673704110fbf749617b5c 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -855,14 +855,14 @@ impl Conversation { ) -> Self { let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); buffer.set_language_registry(language_registry); cx.spawn_weak(|buffer, mut cx| async move { let markdown = markdown.await?; let buffer = buffer .upgrade(&cx) .ok_or_else(|| anyhow!("buffer was dropped"))?; - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) }); anyhow::Ok(()) @@ -944,7 +944,7 @@ impl Conversation { let mut message_anchors = Vec::new(); let mut next_message_id = MessageId(0); let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, saved_conversation.text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); for message in saved_conversation.messages { message_anchors.push(MessageAnchor { id: message.id, @@ -958,7 +958,7 @@ impl Conversation { let buffer = buffer .upgrade(&cx) .ok_or_else(|| anyhow!("buffer was dropped"))?; - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) }); anyhow::Ok(()) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index ab2d861190ff98fb7b4da954a7b92bfb43d75a9d..427134894f3a7383febb571357bf39083e9b06cc 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -980,7 +980,7 @@ mod tests { deterministic.forbid_parking(); let (copilot, mut lsp) = Copilot::fake(cx); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello")); let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); assert_eq!( @@ -996,7 +996,7 @@ mod tests { } ); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx)); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye")); let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); assert_eq!( diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 611866bcadeaef851ba081434fadc04a2d3031ae..5698ccede14bdfd80f42135b54b36efa01f9b8e1 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1362,7 +1362,8 @@ pub mod tests { cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1451,7 +1452,8 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1523,7 +1525,8 @@ pub mod tests { let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2d934a1dc8f213d35648e3fd5799ccf8c08321c3..38b91007294040d4824cc79231de0a092ba9207a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1271,7 +1271,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx) } @@ -1280,7 +1280,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) } @@ -1290,7 +1290,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( EditorMode::AutoHeight { max_lines }, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 25a5d45282d580aab5a93bf8ad3750250486e42a..fbc8a0b23543e80716aa9986c3a15a1d9e4acad7 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -42,7 +42,7 @@ fn test_edit_events(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, "123456", cx); + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); buffer.set_group_interval(Duration::from_secs(1)); buffer }); @@ -174,7 +174,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut now = Instant::now(); - let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); + let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx @@ -247,7 +247,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, "abcde", cx); + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); // Ensure automatic grouping doesn't occur. buffer.set_group_interval(Duration::ZERO); buffer @@ -2281,10 +2281,12 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { None, )); - let toml_buffer = - cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx)); + let toml_buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) + }); let rust_buffer = cx.add_model(|cx| { - Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx) + Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") + .with_language(rust_language, cx) }); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -3754,7 +3756,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -3917,7 +3920,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor @@ -4480,7 +4484,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -4628,7 +4633,8 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor @@ -5834,7 +5840,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( @@ -5918,7 +5924,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { primary: None, } }); - let buffer = cx.add_model(|cx| Buffer::new(0, initial_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts(buffer, excerpt_ranges, cx); @@ -5976,7 +5982,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { fn test_refresh_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -6063,7 +6069,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -6160,7 +6166,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { "{{} }\n", // ); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -7160,8 +7167,8 @@ async fn test_copilot_multibuffer( let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 6b3032b2a35ba9fd46ec145953e626a0f4914f98..def6340e389367c0e483c9648e377d3d92b68c57 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -756,7 +756,8 @@ mod tests { .select_font(family_id, &Default::default()) .unwrap(); - let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e84cfb85aa0ce0a1d0525b1b40268923852f6d4f..3ace5adbc7f46a0d616fe2ebad554a62f6e95901 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1570,7 +1570,7 @@ impl MultiBuffer { #[cfg(any(test, feature = "test-support"))] impl MultiBuffer { pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> ModelHandle { - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); cx.add_model(|cx| Self::singleton(buffer, cx)) } @@ -1580,7 +1580,7 @@ impl MultiBuffer { ) -> 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 buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { context: range, primary: None, @@ -1672,7 +1672,7 @@ impl MultiBuffer { if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) { let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() { let text = RandomCharIter::new(&mut *rng).take(10).collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx))); + buffers.push(cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text))); let buffer = buffers.last().unwrap().read(cx); log::info!( "Creating new buffer {} with text: {:?}", @@ -4022,7 +4022,8 @@ mod tests { #[gpui::test] fn test_singleton(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -4049,7 +4050,7 @@ mod tests { #[gpui::test] fn test_remote(cx: &mut AppContext) { - let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx)); + let host_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a")); let guest_buffer = cx.add_model(|cx| { let state = host_buffer.read(cx).to_proto(); let ops = cx @@ -4080,8 +4081,10 @@ mod tests { #[gpui::test] fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx)); + let buffer_1 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); + let buffer_2 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'g'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let events = Rc::new(RefCell::new(Vec::::new())); @@ -4314,8 +4317,10 @@ mod tests { #[gpui::test] fn test_excerpt_events(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx)); + let buffer_1 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'a'))); + let buffer_2 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'm'))); let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); @@ -4420,7 +4425,8 @@ mod tests { #[gpui::test] fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts_with_context_lines( @@ -4456,7 +4462,8 @@ mod tests { #[gpui::test] async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); @@ -4502,7 +4509,7 @@ mod tests { #[gpui::test] fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let old_snapshot = multibuffer.read(cx).snapshot(cx); buffer.update(cx, |buffer, cx| { @@ -4522,8 +4529,8 @@ mod tests { #[gpui::test] fn test_multibuffer_anchors(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "efghi")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( @@ -4580,8 +4587,8 @@ mod tests { #[gpui::test] fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "ABCDEFGHIJKLMNOP")); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); // Create an insertion id in buffer 1 that doesn't exist in buffer 2. @@ -4976,7 +4983,9 @@ mod tests { let base_text = util::RandomCharIter::new(&mut rng) .take(10) .collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx))); + buffers.push( + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text)), + ); buffers.last().unwrap() } else { buffers.choose(&mut rng).unwrap() @@ -5317,8 +5326,8 @@ mod tests { fn test_history(cx: &mut AppContext) { cx.set_global(SettingsStore::test(cx)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "1234")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "5678")); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let group_interval = multibuffer.read(cx).history.group_interval; multibuffer.update(cx, |multibuffer, cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d5586af5ef8fd9088685a6934c16ea62169d953f..902ed26b57471dce6a9dadcdca0a00dd0a8b9051 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -347,13 +347,9 @@ impl CharKind { } impl Buffer { - pub fn new>( - replica_id: ReplicaId, - base_text: T, - cx: &mut ModelContext, - ) -> Self { + pub fn new>(replica_id: ReplicaId, id: u64, base_text: T) -> Self { Self::build( - TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), + TextBuffer::new(replica_id, id, base_text.into()), None, None, ) diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 9d4b9c38fe287596144fecc731bd59398ec10c0b..db3749aa251517c690c49d25167a640534941a21 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -43,8 +43,8 @@ fn test_line_endings(cx: &mut gpui::AppContext) { init_settings(cx, |_| {}); cx.add_model(|cx| { - let mut buffer = - Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "one\r\ntwo\rthree") + .with_language(Arc::new(rust_lang()), cx); assert_eq!(buffer.text(), "one\ntwo\nthree"); assert_eq!(buffer.line_ending(), LineEnding::Windows); @@ -138,8 +138,8 @@ fn test_edit_events(cx: &mut gpui::AppContext) { let buffer_1_events = Rc::new(RefCell::new(Vec::new())); let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); - let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); + let buffer1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcdef")); + let buffer2 = cx.add_model(|cx| Buffer::new(1, cx.model_id() as u64, "abcdef")); let buffer1_ops = Rc::new(RefCell::new(Vec::new())); buffer1.update(cx, { let buffer1_ops = buffer1_ops.clone(); @@ -222,7 +222,7 @@ fn test_edit_events(cx: &mut gpui::AppContext) { #[gpui::test] async fn test_apply_diff(cx: &mut gpui::TestAppContext) { let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); let anchor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))); let text = "a\nccc\ndddd\nffffff\n"; @@ -254,7 +254,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { ] .join("\n"); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); // Spawn a task to format the buffer's whitespace. // Pause so that the foratting task starts running. @@ -318,8 +318,9 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_reparse(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); // Wait for the initial text to parse buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; @@ -443,7 +444,8 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_resetting_language(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "{}", cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, "{}").with_language(Arc::new(rust_lang()), cx); buffer.set_sync_parse_timeout(Duration::ZERO); buffer }); @@ -491,8 +493,9 @@ async fn test_outline(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let outline = buffer .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -576,8 +579,9 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let outline = buffer .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -613,7 +617,9 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx) + }); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); // extra context nodes are included in the outline. @@ -655,8 +661,9 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); // point is at the start of an item @@ -877,7 +884,8 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: & fn test_range_for_syntax_ancestor(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() { b(|c| {}) }"; - let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); let snapshot = buffer.snapshot(); assert_eq!( @@ -917,7 +925,8 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); @@ -959,7 +968,8 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); @@ -1000,6 +1010,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { c; @@ -1007,7 +1018,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC } " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1073,6 +1083,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { b(); @@ -1080,7 +1091,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC " .replace("|", "") // marker to preserve trailing whitespace .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1136,13 +1146,13 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { i } " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1198,11 +1208,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() {} " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1254,7 +1264,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { cx.add_model(|cx| { let text = "a\nb"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(0..1, "\n"), (2..3, "\n")], Some(AutoindentMode::EachLine), @@ -1280,7 +1291,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { " .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], Some(AutoindentMode::EachLine), @@ -1317,7 +1329,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) { } "# .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); // When this text was copied, both of the quotation marks were at the same // indent level, but the indentation of the first line was not included in @@ -1402,7 +1415,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex } "# .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); // The original indent columns are not known, so this text is // auto-indented in a block as if the first line was copied in @@ -1481,7 +1495,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { " .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language( + let mut buffer = Buffer::new(0, cx.model_id() as u64, text).with_language( Arc::new(Language::new( LanguageConfig { name: "Markdown".into(), @@ -1557,7 +1571,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) { false, ); - let mut buffer = Buffer::new(0, text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, text); buffer.set_language_registry(language_registry); buffer.set_language(Some(html_language), cx); buffer.edit( @@ -1593,7 +1607,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { }); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, "").with_language(Arc::new(ruby_lang()), cx); let text = r#" class C @@ -1683,7 +1698,8 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) { let text = r#"a["b"] = ;"#; - let buffer = Buffer::new(0, text, cx).with_language(Arc::new(language), cx); + let buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx); let snapshot = buffer.snapshot(); let config = snapshot.language_scope_at(0).unwrap(); @@ -1762,7 +1778,8 @@ fn test_language_scope_at_with_rust(cx: &mut AppContext) { "# .unindent(); - let buffer = Buffer::new(0, text.clone(), cx).with_language(Arc::new(language), cx); + let buffer = Buffer::new(0, cx.model_id() as u64, text.clone()) + .with_language(Arc::new(language), cx); let snapshot = buffer.snapshot(); // By default, all brackets are enabled @@ -1806,7 +1823,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { language_registry.add(Arc::new(html_lang())); language_registry.add(Arc::new(erb_lang())); - let mut buffer = Buffer::new(0, text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, text); buffer.set_language_registry(language_registry.clone()); buffer.set_language( language_registry @@ -1838,7 +1855,7 @@ fn test_serialization(cx: &mut gpui::AppContext) { let mut now = Instant::now(); let buffer1 = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "abc", cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "abc"); buffer.edit([(3..3, "D")], None, cx); now += Duration::from_secs(1); @@ -1893,7 +1910,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let mut replica_ids = Vec::new(); let mut buffers = Vec::new(); let network = Rc::new(RefCell::new(Network::new(rng.clone()))); - let base_buffer = cx.add_model(|cx| Buffer::new(0, base_text.as_str(), cx)); + let base_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text.as_str())); for i in 0..rng.gen_range(min_peers..=max_peers) { let buffer = cx.add_model(|cx| { @@ -2394,7 +2411,8 @@ fn assert_bracket_pairs( ) { let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false); let buffer = cx.add_model(|cx| { - Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx) + Buffer::new(0, cx.model_id() as u64, expected_text.clone()) + .with_language(Arc::new(language), cx) }); let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot()); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d18f57ffe95386d3b986e30a8cddad27c4f250fd..51bdb4c5cece790604a31d962fc7e2cef0297f98 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -176,7 +176,9 @@ impl LogStore { cx.notify(); LanguageServerState { rpc_state: None, - log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(), + log_buffer: cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) + .clone(), } }) .log_buffer @@ -241,7 +243,7 @@ impl LogStore { let rpc_state = server_state.rpc_state.get_or_insert_with(|| { let io_tx = self.io_tx.clone(); let language = project.read(cx).languages().language_for_name("JSON"); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); cx.spawn_weak({ let buffer = buffer.clone(); |_, mut cx| async move { @@ -327,7 +329,7 @@ impl LspLogView { .projects .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); let mut this = Self { editor: Self::editor_for_buffer(project.clone(), buffer, cx), project, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 547c7dcc959b32423699b250d03ec13991da03fb..f839c8d5c504b93f6b11e31d6e39a6f0284f1255 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1568,9 +1568,9 @@ impl Project { if self.is_remote() { return Err(anyhow!("creating buffers as a guest is not supported yet")); } - + let id = post_inc(&mut self.next_buffer_id); let buffer = cx.add_model(|cx| { - Buffer::new(self.replica_id(), text, cx) + Buffer::new(self.replica_id(), id, text) .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) }); self.register_buffer(&buffer, cx)?; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 4078cb572d6dcc57320f04d1bf7e13504dcbd521..e708781eca232702eea6c9ca7bf4b0e4ecf1ee05 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -837,6 +837,7 @@ mod tests { let buffer = cx.add_model(|cx| { Buffer::new( 0, + cx.model_id() as u64, r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -844,7 +845,6 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(), - cx, ) }); let window = cx.add_window(|_| EmptyView); @@ -1225,7 +1225,7 @@ mod tests { expected_query_matches_count > 1, "Should pick a query with multiple results" ); - let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); let window = cx.add_window(|_| EmptyView); let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); @@ -1412,7 +1412,7 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); let window = cx.add_window(|_| EmptyView); let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 47aa2b739c3773fe701552a0ac17477c15ee963b..c5041136c9eda608593373a08a57d46d27c0cafd 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -289,7 +289,7 @@ mod tests { let language = crate::languages::language("c", tree_sitter_c::language(), None).await; cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); // empty function buffer.edit([(0..0, "int main() {}")], None, cx); diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 331d829cbc78f24bcc5a641bc11c556de2cd11bc..d89a4171e93e3a006180225ab8786b41d921f2e9 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -210,7 +210,7 @@ mod tests { }); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 3c7f84fec7dced7f8241ff7009160b0d748191f4..d550d126bb1ee03a61b33740a0dc36a286eb84b9 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -474,7 +474,7 @@ mod tests { let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); // indent between braces buffer.set_text("fn a() {}", cx); diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 0a47d365b598aa41df1c1fa50aedd7d718aceb87..34a512f300584f38eaac4905af4b6a772a012ab7 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -356,8 +356,9 @@ mod tests { "# .unindent(); - let buffer = - cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx.add_model(|cx| { + language::Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx) + }); let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); assert_eq!( outline From 9aad602af7b71e24377518e9dee9febc7ab96779 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 28 Aug 2023 18:20:10 +0200 Subject: [PATCH 122/142] chore: Bump memchr to 2.6.0 (#2902) Fresh off the press, memchr 2.6.0 adds vector search routines for aarch64. That directly improves our search performance for both text and regex searches. Per BurntSushi's claims, the simple string searches in ripgrep got ~2 times faster (more details available in https://github.com/BurntSushi/memchr/pull/129). Release Notes: - N/A --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfdb1b6092d1b221697a356a845e2db3ee4d6b02..347976691d7bc138cff4b6202314c8afcea02764 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4269,9 +4269,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" [[package]] name = "memfd" From 04354675ca2d2b9bedebcd4f2a5191289aaad9de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 10:35:23 -0700 Subject: [PATCH 123/142] Remove search dismiss button --- crates/search/src/buffer_search.rs | 9 +-------- crates/search/src/project_search.rs | 11 ----------- crates/search/src/search_bar.rs | 28 ---------------------------- 3 files changed, 1 insertion(+), 47 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e708781eca232702eea6c9ca7bf4b0e4ecf1ee05..01d4b7693f1b5a8509df1f45b55d2a9343910dba 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -214,7 +214,7 @@ impl View for BufferSearchBar { let icon_style = theme.search.editor_icon.clone(); let nav_column = Flex::row() - .with_child(self.render_action_button("Select All", cx)) + .with_child(self.render_action_button("all", cx)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) .with_child(Flex::row().with_children(match_count)) @@ -268,13 +268,6 @@ impl View for BufferSearchBar { .contained() .with_style(theme.search.modes_container), ) - .with_child(super::search_bar::render_close_button( - "Dismiss Buffer Search", - &theme.search, - cx, - |_, this, cx| this.dismiss(&Default::default(), cx), - Some(Box::new(Dismiss)), - )) .constrained() .with_height(theme.search.search_bar_row_height) .aligned() diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6364183877b7e1f2bff8ea26b6244a0ed57c6be9..8542124a2566301b7aa459bd12e8741bdf66f72c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1597,17 +1597,6 @@ impl View for ProjectSearchBar { .contained() .with_style(theme.search.modes_container), ) - .with_child(super::search_bar::render_close_button( - "Dismiss Project Search", - &theme.search, - cx, - |_, this, cx| { - if let Some(search) = this.active_project_search.as_mut() { - search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) - } - }, - None, - )) .constrained() .with_height(theme.search.search_bar_row_height) .aligned() diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 7d3c5261ea759c9a58d5442de4630621c87044f9..4676b8f0279d89c171d3a2a60a52b160b8016f9c 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -13,34 +13,6 @@ use crate::{ SelectNextMatch, SelectPrevMatch, }; -pub(super) fn render_close_button( - tooltip: &'static str, - theme: &theme::Search, - cx: &mut ViewContext, - on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, - dismiss_action: Option>, -) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum CloseButton {} - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.search_bar_row_height) - }) - .on_click(MouseButton::Left, on_click) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::(0, tooltip.to_string(), dismiss_action, tooltip_style, cx) - .into_any() -} - pub(super) fn render_nav_button( icon: &'static str, direction: Direction, From a1d2ae3095a7e279cbd202ab6bbbd97f06e7035e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 28 Aug 2023 13:32:30 -0600 Subject: [PATCH 124/142] Add -l option to build script When you pass -l, we build for the local architecture only and copy the resulting app bundle to /Applications. You can provide a bundle name as an optional argument. --- script/bundle | 163 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 117 insertions(+), 46 deletions(-) diff --git a/script/bundle b/script/bundle index 9f50862cd590e12fb30a3cc896aaa403652b45ac..49da1072ceaff28bff27e101caadf85e58d2e2c4 100755 --- a/script/bundle +++ b/script/bundle @@ -5,11 +5,29 @@ set -e build_flag="--release" target_dir="release" open_result=false +local_only=false +overwrite_local_app=false +bundle_name="" + +# Function for displaying help info +help_info() { + echo " +Usage: ${0##*/} [options] [bundle_name] +Build the application bundle. + +Options: + -d Compile in debug mode and print the app bundle's path. + -l Compile for local architecture only and copy bundle to /Applications. + -o Open the resulting DMG or the app itself in local mode. + -f Overwrite the local app bundle if it exists. + -h Display this help and exit. + " +} # If -o option is specified, the folder of the resulting dmg will be opened in finder # If -d is specified, Zed will be compiled in debug mode and the application's path printed # If -od or -do is specified Zed will be bundled in debug and the application will be run. -while getopts 'od' flag +while getopts 'dlfoh' flag do case "${flag}" in o) open_result=true;; @@ -17,9 +35,21 @@ do build_flag=""; target_dir="debug" ;; + l) local_only=true;; + f) overwrite_local_app=true;; + h) + help_info + exit 0 + ;; esac done +shift $((OPTIND-1)) + +if [ "$1" ]; then + bundle_name=$1 +fi + export ZED_BUNDLE=true export MACOSX_DEPLOYMENT_TARGET=10.15.7 @@ -33,14 +63,24 @@ rustup target add wasm32-wasi # Deal with versions of macOS that don't include libstdc++ headers export CXXFLAGS="-stdlib=libc++" -echo "Compiling zed binary for aarch64-apple-darwin" -cargo build ${build_flag} --package zed --target aarch64-apple-darwin -echo "Compiling zed binary for x86_64-apple-darwin" -cargo build ${build_flag} --package zed --target x86_64-apple-darwin -echo "Compiling cli binary for aarch64-apple-darwin" -cargo build ${build_flag} --package cli --target aarch64-apple-darwin -echo "Compiling cli binary for x86_64-apple-darwin" -cargo build ${build_flag} --package cli --target x86_64-apple-darwin +version_info=$(rustc --version --verbose) +host_line=$(echo "$version_info" | grep host) +local_target_triple=${host_line#*: } + +if [ "$local_only" = true ]; then + echo "Building for local target only." + cargo build ${build_flag} --package zed + cargo build ${build_flag} --package cli +else + echo "Compiling zed binary for aarch64-apple-darwin" + cargo build ${build_flag} --package zed --target aarch64-apple-darwin + echo "Compiling zed binary for x86_64-apple-darwin" + cargo build ${build_flag} --package zed --target x86_64-apple-darwin + echo "Compiling cli binary for aarch64-apple-darwin" + cargo build ${build_flag} --package cli --target aarch64-apple-darwin + echo "Compiling cli binary for x86_64-apple-darwin" + cargo build ${build_flag} --package cli --target x86_64-apple-darwin +fi echo "Creating application bundle" pushd crates/zed @@ -50,27 +90,34 @@ sed \ -i .backup \ "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \ Cargo.toml -app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) + +if [ "$local_only" = true ]; then + app_path=$(cargo bundle ${build_flag} --select-workspace-root | xargs) +else + app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) +fi mv Cargo.toml.backup Cargo.toml popd echo "Bundled ${app_path}" -echo "Creating fat binaries" -lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \ - -output \ - "${app_path}/Contents/MacOS/zed" -lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \ - -output \ - "${app_path}/Contents/MacOS/cli" +if [ "$local_only" = false ]; then + echo "Creating fat binaries" + lipo \ + -create \ + target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \ + -output \ + "${app_path}/Contents/MacOS/zed" + lipo \ + -create \ + target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \ + -output \ + "${app_path}/Contents/MacOS/cli" +fi echo "Copying WebRTC.framework into the frameworks folder" mkdir "${app_path}/Contents/Frameworks" -cp -R target/x86_64-apple-darwin/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" @@ -99,31 +146,55 @@ if [ "$target_dir" = "debug" ]; then exit 0 fi -dmg_target_directory="target/${target_dir}" -dmg_source_directory="${dmg_target_directory}/dmg" -dmg_file_path="${dmg_target_directory}/Zed.dmg" - -echo "Creating DMG" -rm -rf ${dmg_source_directory} -mkdir -p ${dmg_source_directory} -mv "${app_path}" "${dmg_source_directory}" - -ln -s /Applications ${dmg_source_directory} -hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}" -# If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`. -# This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now. -rm ${dmg_source_directory}/Applications +if [ "$local_only" = true ]; then + # If bundle_name is not set or empty, use the basename of $app_path + if [ -z "$bundle_name" ]; then + bundle_name=$(basename "$app_path") + else + # If bundle_name doesn't end in .app, append it + if [[ "$bundle_name" != *.app ]]; then + bundle_name="$bundle_name.app" + fi + fi -echo "Adding license agreement to DMG" -npm install --global dmg-license minimist -dmg-license script/eula/eula.json "${dmg_file_path}" + if [ "$overwrite_local_app" = true ]; then + rm -rf "/Applications/$bundle_name" + fi + mv "$app_path" "/Applications/$bundle_name" -if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then - echo "Notarizing DMG with Apple" - npm install -g notarize-cli - npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" -fi + if [ "$open_result" = true ]; then + open "/Applications/$bundle_name" + else + echo "Installed application bundle:" + echo "/Applications/$bundle_name" + fi +else + echo "Creating DMG" + dmg_target_directory="target/${target_dir}" + dmg_source_directory="${dmg_target_directory}/dmg" + dmg_file_path="${dmg_target_directory}/Zed.dmg" + + rm -rf ${dmg_source_directory} + mkdir -p ${dmg_source_directory} + mv "${app_path}" "${dmg_source_directory}" + + ln -s /Applications ${dmg_source_directory} + hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}" + # If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`. + # This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now. + rm ${dmg_source_directory}/Applications + + echo "Adding license agreement to DMG" + npm install --global dmg-license minimist + dmg-license script/eula/eula.json "${dmg_file_path}" + + if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then + echo "Notarizing DMG with Apple" + npm install -g notarize-cli + npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" + fi -if [ "$open_result" = true ]; then - open $dmg_target_directory + if [ "$open_result" = true ]; then + open $dmg_target_directory + fi fi From 9521f6da42c30291660d6294b07ddb1349161d94 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:16:21 -0700 Subject: [PATCH 125/142] Simplify implementation of flex with spacing --- crates/gpui/src/elements/flex.rs | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index d43a152a6499041b1ea23884f2493f7ef202f0b4..80dfb0625cd581407801148a5df7ee2046eae596 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -88,8 +88,7 @@ impl Flex { cx: &mut LayoutContext, ) { let cross_axis = self.axis.invert(); - let last = self.children.len() - 1; - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { if let Some(metadata) = child.metadata::() { if let Some((flex, expanded)) = metadata.flex { if expanded != layout_expanded { @@ -101,10 +100,6 @@ impl Flex { } else { let space_per_flex = *remaining_space / *remaining_flex; space_per_flex * flex - } - if ix == 0 || ix == last { - self.spacing / 2. - } else { - self.spacing }; let child_min = if expanded { child_max } else { 0. }; let child_constraint = match self.axis { @@ -144,13 +139,12 @@ impl Element for Flex { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { let mut total_flex = None; - let mut fixed_space = 0.0; + let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing; let mut contains_float = false; let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; - let last = self.children.len().saturating_sub(1); - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { let metadata = child.metadata::(); contains_float |= metadata.map_or(false, |metadata| metadata.float); @@ -168,12 +162,7 @@ impl Element for Flex { ), }; let size = child.layout(child_constraint, view, cx); - fixed_space += size.along(self.axis) - + if ix == 0 || ix == last { - self.spacing / 2. - } else { - self.spacing - }; + fixed_space += size.along(self.axis); cross_axis_max = cross_axis_max.max(size.along(cross_axis)); } } @@ -333,8 +322,7 @@ impl Element for Flex { } } - let last = self.children.len().saturating_sub(1); - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -372,11 +360,9 @@ impl Element for Flex { child.paint(scene, aligned_child_origin, visible_bounds, view, cx); - let spacing = if ix == last { 0. } else { self.spacing }; - match self.axis { - Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0), - Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing), + Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing), } } From bb448b91d5404fdab1b86bda34d7ad68882b2001 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:16:41 -0700 Subject: [PATCH 126/142] Don't add a quick actions toolbar item for non-editor views Rather than adding primary toolbar item that renders as empty, don't add an item at all. This prevents spurious spacing from being added after other primary toolbar items. --- crates/quick_action_bar/src/quick_action_bar.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 1804c2b1fce77054cc21b4d5f119da32531a8bf8..8595645e59b873f9cf3b5d7d4c596475eab4fb0e 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -152,9 +152,10 @@ impl ToolbarItemView for QuickActionBar { cx.notify(); } })); + ToolbarItemLocation::PrimaryRight { flex: None } + } else { + ToolbarItemLocation::Hidden } - - ToolbarItemLocation::PrimaryRight { flex: None } } None => { self.active_item = None; From 78f9a1f280a3dc77f7f26941c77ec57319e7a073 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:18:30 -0700 Subject: [PATCH 127/142] Remove padding from workspace toolbar, increase its content height to compensate The padding makes it difficult to layout toolbar items correctly when they are more than one row tall. --- styles/src/style_tree/workspace.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index ecfb572f7e892944485fe223eceb210e76c653fc..43a6cec58537ebf3cefd6eb3ee3e5c44d0c2066d 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -129,7 +129,7 @@ export default function workspace(): any { status_bar: statusBar(), titlebar: titlebar(), toolbar: { - height: 34, + height: 42, background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 8, @@ -138,7 +138,7 @@ export default function workspace(): any { variant: "ghost", active_color: "accent", }), - padding: { left: 8, right: 8, top: 4, bottom: 4 }, + padding: { left: 8, right: 8 }, }, breadcrumb_height: 24, breadcrumbs: interactive({ From 3eee282a6b72dc15971ed8bfbe2f1c29ed8caa4e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:20:09 -0700 Subject: [PATCH 128/142] Overhaul search bar layout * Use a single row, instead of centering the search bar within a double-row toolbar. * Search query controls on the left, navigation on the right * Semantic is the final mode, for greater stability between buffer and project search. * Prevent query editor from moving when toggling path filters --- crates/search/src/buffer_search.rs | 74 +++++----- crates/search/src/mode.rs | 23 --- crates/search/src/project_search.rs | 216 ++++++++++++++-------------- crates/search/src/search_bar.rs | 27 ++-- crates/theme/src/theme.rs | 2 +- crates/workspace/src/toolbar.rs | 13 +- styles/src/style_tree/search.ts | 41 +----- 7 files changed, 164 insertions(+), 232 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 01d4b7693f1b5a8509df1f45b55d2a9343910dba..26ae86f375a134e198299a55989c7cfc0cb71027 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,6 @@ use crate::{ history::SearchHistory, - mode::{next_mode, SearchMode}, + mode::{next_mode, SearchMode, Side}, search_bar::{render_nav_button, render_search_mode_button}, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, @@ -156,11 +156,12 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); - let search_button_for_mode = |mode, cx: &mut ViewContext| { + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = self.current_mode == mode; render_search_mode_button( mode, + side, is_active, move |_, this, cx| { this.activate_search_mode(mode, cx); @@ -212,20 +213,11 @@ impl View for BufferSearchBar { ) }; - let icon_style = theme.search.editor_icon.clone(); - let nav_column = Flex::row() - .with_child(self.render_action_button("all", cx)) - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(Flex::row().with_children(match_count)) - .constrained() - .with_height(theme.search.search_bar_row_height); - - let query = Flex::row() + let query_column = Flex::row() .with_child( - Svg::for_style(icon_style.icon) + Svg::for_style(theme.search.editor_icon.clone().icon) .contained() - .with_style(icon_style.container), + .with_style(theme.search.editor_icon.clone().container), ) .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) .with_child( @@ -244,42 +236,45 @@ impl View for BufferSearchBar { .contained(), ) .align_children_center() - .flex(1., true); - let editor_column = Flex::row() - .with_child( - query - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) .contained() + .with_style(query_container_style) .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) .with_height(theme.search.search_bar_row_height) .flex(1., false); + let mode_column = Flex::row() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .contained() - .with_style(theme.search.modes_container), - ) + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + Some(Side::Right), + cx, + )) + .contained() + .with_style(theme.search.modes_container) + .constrained() + .with_height(theme.search.search_bar_row_height); + + let nav_column = Flex::row() + .with_child(Flex::row().with_children(match_count)) + .with_child(self.render_action_button("all", cx)) + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .right() .flex_float(); + Flex::row() - .with_child(editor_column) - .with_child(nav_column) + .with_child(query_column) .with_child(mode_column) + .with_child(nav_column) .contained() .with_style(theme.search.container) - .aligned() .into_any_named("search bar") } } @@ -333,8 +328,9 @@ impl ToolbarItemView for BufferSearchBar { ToolbarItemLocation::Hidden } } + fn row_count(&self, _: &ViewContext) -> usize { - 2 + 1 } } diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 2c180be761ef9e4132a35d6cb47e7962e431e90c..56d6dfa024bae6d83243bb30697f59330a0dbb9a 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -48,29 +48,6 @@ impl SearchMode { SearchMode::Regex => Box::new(ActivateRegexMode), } } - - pub(crate) fn border_right(&self) -> bool { - match self { - SearchMode::Regex => true, - SearchMode::Text => true, - SearchMode::Semantic => true, - } - } - - pub(crate) fn border_left(&self) -> bool { - match self { - SearchMode::Text => true, - _ => false, - } - } - - pub(crate) fn button_side(&self) -> Option { - match self { - SearchMode::Text => Some(Side::Left), - SearchMode::Semantic => None, - SearchMode::Regex => Some(Side::Right), - } - } } pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8542124a2566301b7aa459bd12e8741bdf66f72c..c2ecde4ce585081cf0f1d76a3c5be1c61d10b8cc 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,6 +1,6 @@ use crate::{ history::SearchHistory, - mode::SearchMode, + mode::{SearchMode, Side}, search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, @@ -1424,8 +1424,13 @@ impl View for ProjectSearchBar { }, cx, ); + let search = _search.read(cx); + let is_semantic_available = SemanticIndex::enabled(cx); let is_semantic_disabled = search.semantic_state.is_none(); + let icon_style = theme.search.editor_icon.clone(); + let is_active = search.active_match_index.is_some(); + let render_option_button_icon = |path, option, cx: &mut ViewContext| { crate::search_bar::render_option_button_icon( self.is_option_enabled(option, cx), @@ -1451,28 +1456,23 @@ impl View for ProjectSearchBar { render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) }); - let search = _search.read(cx); - let icon_style = theme.search.editor_icon.clone(); - - // Editor Functionality - let query = Flex::row() - .with_child( - Svg::for_style(icon_style.icon) - .contained() - .with_style(icon_style.container), - ) - .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) - .with_child( - Flex::row() - .with_child(filter_button) - .with_children(case_sensitive) - .with_children(whole_word) - .flex(1., false) - .constrained() - .contained(), + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + render_search_mode_button( + mode, + side, + is_active, + move |_, this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, ) - .align_children_center() - .flex(1., true); + }; let search = _search.read(cx); @@ -1490,50 +1490,6 @@ impl View for ProjectSearchBar { theme.search.include_exclude_editor.input.container }; - let included_files_view = ChildView::new(&search.included_files_editor, cx) - .contained() - .flex(1., true); - let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) - .contained() - .flex(1., true); - let filters = search.filters_enabled.then(|| { - Flex::row() - .with_child( - included_files_view - .contained() - .with_style(include_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width), - ) - .with_child( - excluded_files_view - .contained() - .with_style(exclude_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width), - ) - .contained() - .with_padding_top(theme.workspace.toolbar.container.padding.bottom) - }); - - let editor_column = Flex::column() - .with_child( - query - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) - .with_children(filters) - .flex(1., false); - let matches = search.active_match_index.map(|match_ix| { Label::new( format!( @@ -1548,25 +1504,81 @@ impl View for ProjectSearchBar { .aligned() }); - let search_button_for_mode = |mode, cx: &mut ViewContext| { - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == mode - } else { - false - }; - render_search_mode_button( - mode, - is_active, - move |_, this, cx| { - this.activate_search_mode(mode, cx); - }, - cx, + let query_column = Flex::column() + .with_spacing(theme.search.search_row_spacing) + .with_child( + Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container), + ) + .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) + .with_child( + Flex::row() + .with_child(filter_button) + .with_children(case_sensitive) + .with_children(whole_word) + .flex(1., false) + .constrained() + .contained(), + ) + .align_children_center() + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false), ) - }; - let is_active = search.active_match_index.is_some(); - let semantic_index = SemanticIndex::enabled(cx) - .then(|| search_button_for_mode(SearchMode::Semantic, cx)); + .with_children(search.filters_enabled.then(|| { + Flex::row() + .with_child( + ChildView::new(&search.included_files_editor, cx) + .contained() + .with_style(include_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .with_child( + ChildView::new(&search.excluded_files_editor, cx) + .contained() + .with_style(exclude_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false) + })) + .flex(1., false); + + let mode_column = + Flex::row() + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + if is_semantic_available { + None + } else { + Some(Side::Right) + }, + cx, + )) + .with_children(is_semantic_available.then(|| { + search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) + })) + .contained() + .with_style(theme.search.modes_container); + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { render_nav_button( label, @@ -1582,32 +1594,17 @@ impl View for ProjectSearchBar { }; let nav_column = Flex::row() + .with_child(Flex::row().with_children(matches)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(Flex::row().with_children(matches)) - .constrained() - .with_height(theme.search.search_bar_row_height); - - let mode_column = Flex::row() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_children(semantic_index) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .contained() - .with_style(theme.search.modes_container), - ) .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .right() - .top() .flex_float(); Flex::row() - .with_child(editor_column) - .with_child(nav_column) + .with_child(query_column) .with_child(mode_column) + .with_child(nav_column) .contained() .with_style(theme.search.container) .into_any_named("project search") @@ -1636,7 +1633,7 @@ impl ToolbarItemView for ProjectSearchBar { self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.active_project_search = Some(search); ToolbarItemLocation::PrimaryLeft { - flex: Some((1., false)), + flex: Some((1., true)), } } else { ToolbarItemLocation::Hidden @@ -1644,13 +1641,12 @@ impl ToolbarItemView for ProjectSearchBar { } fn row_count(&self, cx: &ViewContext) -> usize { - self.active_project_search - .as_ref() - .map(|search| { - let offset = search.read(cx).filters_enabled as usize; - 2 + offset - }) - .unwrap_or_else(|| 2) + if let Some(search) = self.active_project_search.as_ref() { + if search.read(cx).filters_enabled { + return 2; + } + } + 1 } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 4676b8f0279d89c171d3a2a60a52b160b8016f9c..d1a5a0380a3b3f210706d2b2fb488c6e6f156834 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -83,6 +83,7 @@ pub(super) fn render_nav_button( pub(crate) fn render_search_mode_button( mode: SearchMode, + side: Option, is_active: bool, on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, cx: &mut ViewContext, @@ -91,41 +92,41 @@ pub(crate) fn render_search_mode_button( enum SearchModeButton {} MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { let theme = theme::current(cx); - let mut style = theme + let style = theme .search .mode_button .in_state(is_active) .style_for(state) .clone(); - style.container.border.left = mode.border_left(); - style.container.border.right = mode.border_right(); - let label = Label::new(mode.label(), style.text.clone()) - .aligned() - .contained(); - let mut container_style = style.container.clone(); - if let Some(button_side) = mode.button_side() { + let mut container_style = style.container; + if let Some(button_side) = side { if button_side == Side::Left { + container_style.border.left = true; container_style.corner_radii = CornerRadii { bottom_right: 0., top_right: 0., ..container_style.corner_radii }; - label.with_style(container_style) } else { + container_style.border.left = false; container_style.corner_radii = CornerRadii { bottom_left: 0., top_left: 0., ..container_style.corner_radii }; - label.with_style(container_style) } } else { + container_style.border.left = false; container_style.corner_radii = CornerRadii::default(); - label.with_style(container_style) } - .constrained() - .with_height(theme.search.search_bar_row_height) + + Label::new(mode.label(), style.text) + .aligned() + .contained() + .with_style(container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9005fc9757a97f1088ec19b12521389f81be9f73..a5faba8eaf2016af7875665b679aa96de2518674 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -437,11 +437,11 @@ pub struct Search { pub match_index: ContainedText, pub major_results_status: TextStyle, pub minor_results_status: TextStyle, - pub dismiss_button: Interactive, pub editor_icon: IconStyle, pub mode_button: Toggleable>, pub nav_button: Toggleable>, pub search_bar_row_height: f32, + pub search_row_spacing: f32, pub option_button_height: f32, pub modes_container: ContainerStyle, } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 72c879d6d401f171f0f3fc442f74d8053d1b4ce2..c3f4bb9723bdb7208e31f75a704babd400819bb1 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -81,10 +81,7 @@ impl View for Toolbar { ToolbarItemLocation::PrimaryLeft { flex } => { primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); - let left_item = ChildView::new(item.as_any(), cx) - .aligned() - .contained() - .with_margin_right(spacing); + let left_item = ChildView::new(item.as_any(), cx).aligned(); if let Some((flex, expanded)) = flex { primary_left_items.push(left_item.flex(flex, expanded).into_any()); } else { @@ -94,11 +91,7 @@ impl View for Toolbar { ToolbarItemLocation::PrimaryRight { flex } => { primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); - let right_item = ChildView::new(item.as_any(), cx) - .aligned() - .contained() - .with_margin_left(spacing) - .flex_float(); + let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float(); if let Some((flex, expanded)) = flex { primary_right_items.push(right_item.flex(flex, expanded).into_any()); } else { @@ -120,7 +113,7 @@ impl View for Toolbar { let container_style = theme.container; let height = theme.height * primary_items_row_count as f32; - let mut primary_items = Flex::row(); + let mut primary_items = Flex::row().with_spacing(spacing); primary_items.extend(primary_left_items); primary_items.extend(primary_right_items); diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 4493634a8e8a73da36a6f00ac40c56110ad50b88..27e8c43a4df6d21db648cf908cc0bd942aaab9b5 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -34,7 +34,7 @@ export default function search(): any { } return { - padding: { top: 16, bottom: 16, left: 16, right: 16 }, + padding: { top: 4, bottom: 4 }, // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive match_background: with_opacity( foreground(theme.highest, "accent"), @@ -210,6 +210,7 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), padding: { left: 9, + right: 9, }, }, option_button_group: { @@ -232,34 +233,6 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), size: 13, }, - dismiss_button: interactive({ - base: { - color: foreground(theme.highest, "variant"), - icon_width: 14, - button_width: 32, - corner_radius: 6, - padding: { - // // top: 10, - // bottom: 10, - left: 10, - right: 10, - }, - - background: background(theme.highest, "variant"), - - border: border(theme.highest, "on"), - }, - state: { - hovered: { - color: foreground(theme.highest, "hovered"), - background: background(theme.highest, "variant", "hovered") - }, - clicked: { - color: foreground(theme.highest, "pressed"), - background: background(theme.highest, "variant", "pressed") - }, - }, - }), editor_icon: { icon: { color: foreground(theme.highest, "variant"), @@ -375,13 +348,9 @@ export default function search(): any { }) } }), - search_bar_row_height: 32, + search_bar_row_height: 34, + search_row_spacing: 8, option_button_height: 22, - modes_container: { - margin: { - right: 9 - } - } - + modes_container: {} } } From 70bea758979f40c6cff2ed01f75e29a1bade579e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 15:15:54 -0700 Subject: [PATCH 129/142] Change cycle mode action to reflect new mode button order --- crates/search/src/mode.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 56d6dfa024bae6d83243bb30697f59330a0dbb9a..8afc2bd3f496cc502f5ffd53fec5b05973108501 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -51,15 +51,15 @@ impl SearchMode { } pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { - let next_text_state = if semantic_enabled { - SearchMode::Semantic - } else { - SearchMode::Regex - }; - match mode { - SearchMode::Text => next_text_state, - SearchMode::Semantic => SearchMode::Regex, - SearchMode::Regex => SearchMode::Text, + SearchMode::Text => SearchMode::Regex, + SearchMode::Regex => { + if semantic_enabled { + SearchMode::Semantic + } else { + SearchMode::Text + } + } + SearchMode::Semantic => SearchMode::Text, } } From 89eab78cf78c9f9b4b82b2af27ef1c7582c8eaed Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 15:48:55 -0700 Subject: [PATCH 130/142] Debounce document highlight and code actions requests --- crates/editor/src/editor.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 38b91007294040d4824cc79231de0a092ba9207a..537b886b2dbd1cc4ed3860954cbe82e3100ba1b9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -111,6 +111,8 @@ const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); +const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); @@ -3292,7 +3294,7 @@ impl Editor { } fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { - let project = self.project.as_ref()?; + let project = self.project.clone()?; let buffer = self.buffer.read(cx); let newest_selection = self.selections.newest_anchor().clone(); let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; @@ -3301,11 +3303,15 @@ impl Editor { return None; } - let actions = project.update(cx, |project, cx| { - project.code_actions(&start_buffer, start..end, cx) - }); self.code_actions_task = Some(cx.spawn(|this, mut cx| async move { - let actions = actions.await; + cx.background().timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT).await; + + let actions = project + .update(&mut cx, |project, cx| { + project.code_actions(&start_buffer, start..end, cx) + }) + .await; + this.update(&mut cx, |this, cx| { this.available_code_actions = actions.log_err().and_then(|actions| { if actions.is_empty() { @@ -3326,7 +3332,7 @@ impl Editor { return None; } - let project = self.project.as_ref()?; + let project = self.project.clone()?; let buffer = self.buffer.read(cx); let newest_selection = self.selections.newest_anchor().clone(); let cursor_position = newest_selection.head(); @@ -3337,12 +3343,19 @@ impl Editor { return None; } - let highlights = project.update(cx, |project, cx| { - project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) - }); - self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move { - if let Some(highlights) = highlights.await.log_err() { + cx.background() + .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT) + .await; + + let highlights = project + .update(&mut cx, |project, cx| { + project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }) + .await + .log_err(); + + if let Some(highlights) = highlights { this.update(&mut cx, |this, cx| { if this.pending_rename.is_some() { return; From 5142049515dc39e99605a67505ff3dd85bf1b59d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 17:45:19 -0700 Subject: [PATCH 131/142] Demote some Peer logging to trace level --- crates/rpc/src/peer.rs | 54 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 72ddfa567b5822540632bce4bf70d26fe192281c..91b914f169a86b974232b5e8f3e988d36cb64d07 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -171,12 +171,12 @@ impl Peer { let this = self.clone(); let response_channels = connection_state.response_channels.clone(); let handle_io = async move { - tracing::debug!(%connection_id, "handle io future: start"); + tracing::trace!(%connection_id, "handle io future: start"); let _end_connection = util::defer(|| { response_channels.lock().take(); this.connections.write().remove(&connection_id); - tracing::debug!(%connection_id, "handle io future: end"); + tracing::trace!(%connection_id, "handle io future: end"); }); // Send messages on this frequency so the connection isn't closed. @@ -188,68 +188,68 @@ impl Peer { futures::pin_mut!(receive_timeout); loop { - tracing::debug!(%connection_id, "outer loop iteration start"); + tracing::trace!(%connection_id, "outer loop iteration start"); let read_message = reader.read().fuse(); futures::pin_mut!(read_message); loop { - tracing::debug!(%connection_id, "inner loop iteration start"); + tracing::trace!(%connection_id, "inner loop iteration start"); futures::select_biased! { outgoing = outgoing_rx.next().fuse() => match outgoing { Some(outgoing) => { - tracing::debug!(%connection_id, "outgoing rpc message: writing"); + tracing::trace!(%connection_id, "outgoing rpc message: writing"); futures::select_biased! { result = writer.write(outgoing).fuse() => { - tracing::debug!(%connection_id, "outgoing rpc message: done writing"); + tracing::trace!(%connection_id, "outgoing rpc message: done writing"); result.context("failed to write RPC message")?; - tracing::debug!(%connection_id, "keepalive interval: resetting after sending message"); + tracing::trace!(%connection_id, "keepalive interval: resetting after sending message"); keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); } _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "outgoing rpc message: writing timed out"); + tracing::trace!(%connection_id, "outgoing rpc message: writing timed out"); Err(anyhow!("timed out writing message"))?; } } } None => { - tracing::debug!(%connection_id, "outgoing rpc message: channel closed"); + tracing::trace!(%connection_id, "outgoing rpc message: channel closed"); return Ok(()) }, }, _ = keepalive_timer => { - tracing::debug!(%connection_id, "keepalive interval: pinging"); + tracing::trace!(%connection_id, "keepalive interval: pinging"); futures::select_biased! { result = writer.write(proto::Message::Ping).fuse() => { - tracing::debug!(%connection_id, "keepalive interval: done pinging"); + tracing::trace!(%connection_id, "keepalive interval: done pinging"); result.context("failed to send keepalive")?; - tracing::debug!(%connection_id, "keepalive interval: resetting after pinging"); + tracing::trace!(%connection_id, "keepalive interval: resetting after pinging"); keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); } _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "keepalive interval: pinging timed out"); + tracing::trace!(%connection_id, "keepalive interval: pinging timed out"); Err(anyhow!("timed out sending keepalive"))?; } } } incoming = read_message => { let incoming = incoming.context("error reading rpc message from socket")?; - tracing::debug!(%connection_id, "incoming rpc message: received"); - tracing::debug!(%connection_id, "receive timeout: resetting"); + tracing::trace!(%connection_id, "incoming rpc message: received"); + tracing::trace!(%connection_id, "receive timeout: resetting"); receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse()); if let proto::Message::Envelope(incoming) = incoming { - tracing::debug!(%connection_id, "incoming rpc message: processing"); + tracing::trace!(%connection_id, "incoming rpc message: processing"); futures::select_biased! { result = incoming_tx.send(incoming).fuse() => match result { Ok(_) => { - tracing::debug!(%connection_id, "incoming rpc message: processed"); + tracing::trace!(%connection_id, "incoming rpc message: processed"); } Err(_) => { - tracing::debug!(%connection_id, "incoming rpc message: channel closed"); + tracing::trace!(%connection_id, "incoming rpc message: channel closed"); return Ok(()) } }, _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "incoming rpc message: processing timed out"); + tracing::trace!(%connection_id, "incoming rpc message: processing timed out"); Err(anyhow!("timed out processing incoming message"))? } } @@ -257,7 +257,7 @@ impl Peer { break; }, _ = receive_timeout => { - tracing::debug!(%connection_id, "receive timeout: delay between messages too long"); + tracing::trace!(%connection_id, "receive timeout: delay between messages too long"); Err(anyhow!("delay between messages too long"))? } } @@ -274,13 +274,13 @@ impl Peer { let response_channels = response_channels.clone(); async move { let message_id = incoming.id; - tracing::debug!(?incoming, "incoming message future: start"); + tracing::trace!(?incoming, "incoming message future: start"); let _end = util::defer(move || { - tracing::debug!(%connection_id, message_id, "incoming message future: end"); + tracing::trace!(%connection_id, message_id, "incoming message future: end"); }); if let Some(responding_to) = incoming.responding_to { - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, @@ -290,7 +290,7 @@ impl Peer { if let Some(tx) = channel { let requester_resumed = oneshot::channel(); if let Err(error) = tx.send((incoming, requester_resumed.0)) { - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to = responding_to, @@ -299,14 +299,14 @@ impl Peer { ); } - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, "incoming response: waiting to resume requester" ); let _ = requester_resumed.1.await; - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, @@ -323,7 +323,7 @@ impl Peer { None } else { - tracing::debug!(%connection_id, message_id, "incoming message: received"); + tracing::trace!(%connection_id, message_id, "incoming message: received"); proto::build_typed_envelope(connection_id, incoming).or_else(|| { tracing::error!( %connection_id, From 791f6cf9e748fe5cb6121b821f23fd614ee29f15 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 17:45:32 -0700 Subject: [PATCH 132/142] Update some tests to reflect code action debouncing --- crates/collab/src/tests/integration_tests.rs | 90 ++++++++++---------- crates/editor/src/editor.rs | 4 +- crates/zed/src/zed.rs | 2 + 3 files changed, 49 insertions(+), 47 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index b1227b9501a4990b9afb68aa72d22efd355defd7..f64a82e32e938a573721ceecffbeb28e23604fda 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -5321,7 +5321,7 @@ async fn test_collaborating_with_code_actions( .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server + let mut requests = fake_language_server .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, @@ -5330,9 +5330,9 @@ async fn test_collaborating_with_code_actions( assert_eq!(params.range.start, lsp::Position::new(0, 0)); assert_eq!(params.range.end, lsp::Position::new(0, 0)); Ok(None) - }) - .next() - .await; + }); + deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; // Move cursor to a location that contains code actions. editor_b.update(cx_b, |editor, cx| { @@ -5342,7 +5342,7 @@ async fn test_collaborating_with_code_actions( cx.focus(&editor_b); }); - fake_language_server + let mut requests = fake_language_server .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, @@ -5394,9 +5394,9 @@ async fn test_collaborating_with_code_actions( ..Default::default() }, )])) - }) - .next() - .await; + }); + deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; // Toggle code actions and wait for them to display. editor_b.update(cx_b, |editor, cx| { @@ -7864,6 +7864,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( client_a.language_registry().add(Arc::clone(&language)); client_b.language_registry().add(language); + // Client A opens a project. client_a .fs() .insert_tree( @@ -7884,6 +7885,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); + // Client B joins the project let project_b = client_b.build_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) @@ -7893,6 +7895,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); cx_a.foreground().start_waiting(); + // The host opens a rust file. let _buffer_a = project_a .update(cx_a, |project, cx| { project.open_local_buffer("/a/main.rs", cx) @@ -7900,7 +7903,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); - let next_call_id = Arc::new(AtomicU32::new(0)); let editor_a = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) @@ -7909,6 +7911,9 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap() .downcast::() .unwrap(); + + // Set up the language server to return an additional inlay hint on each request. + let next_call_id = Arc::new(AtomicU32::new(0)); fake_language_server .handle_request::(move |params, _| { let task_next_call_id = Arc::clone(&next_call_id); @@ -7917,33 +7922,28 @@ async fn test_mutual_editor_inlay_hint_cache_update( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); - let mut new_hints = Vec::with_capacity(current_call_id as usize); - loop { - new_hints.push(lsp::InlayHint { - position: lsp::Position::new(0, current_call_id), - label: lsp::InlayHintLabel::String(current_call_id.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }); - if current_call_id == 0 { - break; - } - current_call_id -= 1; - } - Ok(Some(new_hints)) + let call_count = task_next_call_id.fetch_add(1, SeqCst); + Ok(Some( + (0..=call_count) + .map(|ix| lsp::InlayHint { + position: lsp::Position::new(0, ix), + label: lsp::InlayHintLabel::String(ix.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }) + .collect(), + )) } }) .next() .await .unwrap(); - cx_a.foreground().finish_waiting(); - cx_a.foreground().run_until_parked(); + deterministic.run_until_parked(); let mut edits_made = 1; editor_a.update(cx_a, |editor, _| { @@ -7969,7 +7969,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .downcast::() .unwrap(); - cx_b.foreground().run_until_parked(); + deterministic.run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( vec!["0".to_string(), "1".to_string()], @@ -7990,25 +7990,25 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx.focus(&editor_b); edits_made += 1; }); - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( - vec!["0".to_string(), "1".to_string(), "2".to_string()], + vec![ + "0".to_string(), + "1".to_string(), + "2".to_string(), + "3".to_string() + ], extract_hint_labels(editor), - "Host should get hints from the 1st edit and 1st LSP query" + "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); }); editor_b.update(cx_b, |editor, _| { assert_eq!( - vec![ - "0".to_string(), - "1".to_string(), - "2".to_string(), - "3".to_string() - ], + vec!["0".to_string(), "1".to_string(), "2".to_string(),], extract_hint_labels(editor), "Guest should get hints the 1st edit and 2nd LSP query" ); @@ -8022,8 +8022,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx.focus(&editor_a); edits_made += 1; }); - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( vec![ @@ -8062,8 +8062,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .expect("inlay refresh request failed"); edits_made += 1; - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( vec![ diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 537b886b2dbd1cc4ed3860954cbe82e3100ba1b9..73774a92397fe1eea048a88b725bc863d206fe4b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -111,8 +111,8 @@ const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); -const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); -const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); +pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index de05c259c81d7c63ff856ab93d4162a34f992511..9ea406fc3e91c00ab6b7d3d8ebdfed918099a535 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1706,6 +1706,8 @@ mod tests { .remove_file(Path::new("/root/a/file2"), Default::default()) .await .unwrap(); + cx.foreground().run_until_parked(); + workspace .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) .await From 7c498feb85dc6d704036f8a9f6dcda12da3b8a0c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Aug 2023 10:40:20 +0300 Subject: [PATCH 133/142] Trim off surrounding `[]` when parsing terminal hover links Terminal has to accept `[` and `]` as valid word parts, due to `[slug].tsx` being a valid file name. Yet, terminal has to exclude these to match paths in strings like `[/some/path/[slug].tsx]`. --- crates/terminal/src/terminal.rs | 37 ++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 83ba056485664057a13c731033ccf1557a332fc8..ea919c8f84c9f87e08558309cb2c5adca811c5cf 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -8,7 +8,7 @@ use alacritty_terminal::{ event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event_loop::{EventLoop, Msg, Notifier}, grid::{Dimensions, Scroll as AlacScroll}, - index::{Column, Direction as AlacDirection, Line, Point}, + index::{Boundary, Column, Direction as AlacDirection, Line, Point}, selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ @@ -724,14 +724,13 @@ impl Terminal { self.last_content.size, term.grid().display_offset(), ) - .grid_clamp(term, alacritty_terminal::index::Boundary::Grid); + .grid_clamp(term, Boundary::Grid); let link = term.grid().index(point).hyperlink(); let found_word = if link.is_some() { let mut min_index = point; loop { - let new_min_index = - min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1); + let new_min_index = min_index.sub(term, Boundary::Cursor, 1); if new_min_index == min_index { break; } else if term.grid().index(new_min_index).hyperlink() != link { @@ -743,8 +742,7 @@ impl Terminal { let mut max_index = point; loop { - let new_max_index = - max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1); + let new_max_index = max_index.add(term, Boundary::Cursor, 1); if new_max_index == max_index { break; } else if term.grid().index(new_max_index).hyperlink() != link { @@ -761,11 +759,34 @@ impl Terminal { } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) { let maybe_url_or_path = term.bounds_to_string(*word_match.start(), *word_match.end()); + let original_match = word_match.clone(); + let (sanitized_match, sanitized_word) = + if maybe_url_or_path.starts_with('[') && maybe_url_or_path.ends_with(']') { + ( + Match::new( + word_match.start().add(term, Boundary::Cursor, 1), + word_match.end().sub(term, Boundary::Cursor, 1), + ), + maybe_url_or_path[1..maybe_url_or_path.len() - 1].to_owned(), + ) + } else { + (word_match, maybe_url_or_path) + }; + let is_url = match regex_match_at(term, point, &URL_REGEX) { - Some(url_match) => url_match == word_match, + Some(url_match) => { + // `]` is a valid symbol in the `file://` URL, so the regex match will include it + // consider that when ensuring that the URL match is the same as the original word + if sanitized_match != original_match { + url_match.start() == sanitized_match.start() + && url_match.end() == original_match.end() + } else { + url_match == sanitized_match + } + } None => false, }; - Some((maybe_url_or_path, is_url, word_match)) + Some((sanitized_word, is_url, sanitized_match)) } else { None }; From ea0e5e880e839ab2e0b3fd2e0dd456f418352d9c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 29 Aug 2023 15:56:50 +0200 Subject: [PATCH 134/142] chore: Use IsTerminal trait instead of relying on libc to detect stdout being a terminal (#2908) IsTerminal was added in 1.70. Release Notes: - N/A --- crates/zed/src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index da726eef65d16e9ffeaa84141506c7e1b184a76a..3e0a8a7a073fcd75dae725f900fe09d328b092b5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -31,7 +31,7 @@ use std::{ env, ffi::OsStr, fs::OpenOptions, - io::Write as _, + io::{IsTerminal, Write as _}, os::unix::prelude::OsStrExt, panic, path::{Path, PathBuf}, @@ -635,8 +635,7 @@ async fn load_login_shell_environment() -> Result<()> { } fn stdout_is_a_pty() -> bool { - std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() - && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 } + std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal() } fn collect_path_args() -> Vec { From 53558bc603f0c24883e8e3d95ed4a48edad6e249 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:05:59 -0400 Subject: [PATCH 135/142] Remove baseurl to prevent theme import issue --- styles/tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/styles/tsconfig.json b/styles/tsconfig.json index 281bd74b215bd16426bb6a8f9d68ddeb5a5bea43..c7eaa50eedd05d201a8eb4d99a562c418f9cdb08 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -21,8 +21,7 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "skipLibCheck": true, - "useUnknownInCatchVariables": false, - "baseUrl": "." + "useUnknownInCatchVariables": false }, "exclude": [ "node_modules" From a5b12d535fec69cb3282c7be8385b608c53ba7e3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:06:13 -0400 Subject: [PATCH 136/142] Add margin and padding functions --- styles/src/component/margin.ts | 34 +++++++++++++++++++++++++++++++++ styles/src/component/padding.ts | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 styles/src/component/margin.ts create mode 100644 styles/src/component/padding.ts diff --git a/styles/src/component/margin.ts b/styles/src/component/margin.ts new file mode 100644 index 0000000000000000000000000000000000000000..f6262405f0b150b06085a6e0b639b405991fe6f0 --- /dev/null +++ b/styles/src/component/margin.ts @@ -0,0 +1,34 @@ +type MarginOptions = { + all?: number + left?: number + right?: number + top?: number + bottom?: number +} + +export type MarginStyle = { + top: number + bottom: number + left: number + right: number +} + +export const margin_style = (options: MarginOptions): MarginStyle => { + const { all, top, bottom, left, right } = options + + if (all !== undefined) return { + top: all, + bottom: all, + left: all, + right: all + } + + if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Margin must have at least one value") + + return { + top: top || 0, + bottom: bottom || 0, + left: left || 0, + right: right || 0 + } +} diff --git a/styles/src/component/padding.ts b/styles/src/component/padding.ts new file mode 100644 index 0000000000000000000000000000000000000000..96792bf7661263d39e058310c836c2e34aae5378 --- /dev/null +++ b/styles/src/component/padding.ts @@ -0,0 +1,34 @@ +type PaddingOptions = { + all?: number + left?: number + right?: number + top?: number + bottom?: number +} + +export type PaddingStyle = { + top: number + bottom: number + left: number + right: number +} + +export const padding_style = (options: PaddingOptions): PaddingStyle => { + const { all, top, bottom, left, right } = options + + if (all !== undefined) return { + top: all, + bottom: all, + left: all, + right: all + } + + if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Padding must have at least one value") + + return { + top: top || 0, + bottom: bottom || 0, + left: left || 0, + right: right || 0 + } +} From 05da4b740a05762fa8181905ac544088caa304d1 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:28:06 -0400 Subject: [PATCH 137/142] Update spacing, button heights --- styles/src/style_tree/search.ts | 78 +++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 27e8c43a4df6d21db648cf908cc0bd942aaab9b5..c3dbff4341215db19536712001b05dc044f6e249 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -3,8 +3,21 @@ import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +const search_results = () => { + const theme = useTheme() + + return { + // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive + match_background: with_opacity( + foreground(theme.highest, "accent"), + 0.4 + ), + } +} + export default function search(): any { const theme = useTheme() + const SEARCH_ROW_SPACING = 12 // Search input const editor = { @@ -35,11 +48,7 @@ export default function search(): any { return { padding: { top: 4, bottom: 4 }, - // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive - match_background: with_opacity( - foreground(theme.highest, "accent"), - 0.4 - ), + option_button: toggleable({ base: interactive({ base: { @@ -167,7 +176,9 @@ export default function search(): any { // top: 2, }, margin: { - right: 9, + top: 1, + bottom: 1, + right: SEARCH_ROW_SPACING } }, state: { @@ -210,13 +221,13 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), padding: { left: 9, - right: 9, + right: SEARCH_ROW_SPACING, }, }, option_button_group: { padding: { - left: 12, - right: 12, + left: SEARCH_ROW_SPACING, + right: SEARCH_ROW_SPACING, }, }, include_exclude_inputs: { @@ -233,24 +244,26 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), size: 13, }, + // Input Icon editor_icon: { icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/magnifying_glass_12.svg", + color: foreground(theme.highest, "disabled"), + asset: "icons/magnifying_glass.svg", dimensions: { - width: 12, - height: 12, + width: 14, + height: 14, } }, container: { - margin: { right: 6 }, - padding: { left: 2, right: 2 }, + margin: { right: 4 }, + padding: { left: 1, right: 1 }, } }, + // Toggle group buttons - Text | Regex | Semantic mode_button: toggleable({ base: interactive({ base: { - ...text(theme.highest, "mono", "variant"), + ...text(theme.highest, "mono", "variant", { size: "sm" }), background: background(theme.highest, "variant"), border: { @@ -258,21 +271,24 @@ export default function search(): any { left: false, right: false }, - + margin: { + top: 1, + bottom: 1, + }, padding: { - left: 10, - right: 10, + left: 12, + right: 12, }, corner_radius: 6, }, state: { hovered: { - ...text(theme.highest, "mono", "variant", "hovered"), + ...text(theme.highest, "mono", "variant", "hovered", { size: "sm" }), background: background(theme.highest, "variant", "hovered"), border: border(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "variant", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed", { size: "sm" }), background: background(theme.highest, "variant", "pressed"), border: border(theme.highest, "on", "pressed"), }, @@ -281,20 +297,21 @@ export default function search(): any { state: { active: { default: { - ...text(theme.highest, "mono", "on"), + ...text(theme.highest, "mono", "on", { size: "sm" }), background: background(theme.highest, "on") }, hovered: { - ...text(theme.highest, "mono", "on", "hovered"), + ...text(theme.highest, "mono", "on", "hovered", { size: "sm" }), background: background(theme.highest, "on", "hovered") }, clicked: { - ...text(theme.highest, "mono", "on", "pressed"), + ...text(theme.highest, "mono", "on", "pressed", { size: "sm" }), background: background(theme.highest, "on", "pressed") }, }, }, }), + // Next/Previous Match buttons nav_button: toggleable({ state: { inactive: interactive({ @@ -307,7 +324,10 @@ export default function search(): any { left: false, right: false, }, - + margin: { + top: 1, + bottom: 1, + }, padding: { left: 10, right: 10, @@ -327,7 +347,10 @@ export default function search(): any { left: false, right: false, }, - + margin: { + top: 1, + bottom: 1, + }, padding: { left: 10, right: 10, @@ -351,6 +374,7 @@ export default function search(): any { search_bar_row_height: 34, search_row_spacing: 8, option_button_height: 22, - modes_container: {} + modes_container: {}, + ...search_results() } } From f6faeea7207fe39a7b84c08460ef29a88cb7bef8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:40:46 -0400 Subject: [PATCH 138/142] Add disabled as an option on text_button --- styles/src/component/text_button.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index b911cd5b778c54b2f4b58cd8aa7911b0fd9d553d..ead017a80324d1600c2bb32600216ed7879d5285 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -17,6 +17,7 @@ interface TextButtonOptions { variant?: Button.Variant color?: keyof Theme["lowest"] margin?: Partial + disabled?: boolean text_properties?: TextProperties } @@ -29,6 +30,7 @@ export function text_button({ color, layer, margin, + disabled, text_properties, }: TextButtonOptions = {}) { const theme = useTheme() @@ -65,13 +67,17 @@ export function text_button({ state: { default: { background: background_color, - color: foreground(layer ?? theme.lowest, color), + color: + disabled + ? foreground(layer ?? theme.lowest, "disabled") + : foreground(layer ?? theme.lowest, color), }, - hovered: { - background: background(layer ?? theme.lowest, color, "hovered"), - color: foreground(layer ?? theme.lowest, color, "hovered"), - }, - clicked: { + hovered: + disabled ? {} : { + background: background(layer ?? theme.lowest, color, "hovered"), + color: foreground(layer ?? theme.lowest, color, "hovered"), + }, + clicked: disabled ? {} : { background: background(layer ?? theme.lowest, color, "pressed"), color: foreground(layer ?? theme.lowest, color, "pressed"), }, From f626c61b1e4466543324710d84995d48233aef0f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:40:58 -0400 Subject: [PATCH 139/142] Update action_button style --- styles/src/style_tree/search.ts | 49 ++++++--------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index c3dbff4341215db19536712001b05dc044f6e249..73f35b9dd4b4a4148f93db0172e91864027819fe 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -2,6 +2,7 @@ import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +import { text_button } from "../component/text_button" const search_results = () => { const theme = useTheme() @@ -162,49 +163,13 @@ export default function search(): any { }, }, }), + // Search tool buttons + // HACK: This is not how disabled elements should be created + // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ - base: interactive({ - base: { - ...text(theme.highest, "mono", "disabled"), - background: background(theme.highest, "disabled"), - corner_radius: 6, - border: border(theme.highest, "disabled"), - padding: { - // bottom: 2, - left: 10, - right: 10, - // top: 2, - }, - margin: { - top: 1, - bottom: 1, - right: SEARCH_ROW_SPACING - } - }, - state: { - hovered: {} - }, - }), state: { - active: interactive({ - base: { - ...text(theme.highest, "mono", "on"), - background: background(theme.highest, "on"), - border: border(theme.highest, "on"), - }, - state: { - hovered: { - ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - border: border(theme.highest, "on", "hovered"), - }, - clicked: { - ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), - border: border(theme.highest, "on", "pressed"), - }, - }, - }) + inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING } }), + active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING } }) } }), editor, @@ -312,6 +277,8 @@ export default function search(): any { }, }), // Next/Previous Match buttons + // HACK: This is not how disabled elements should be created + // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled nav_button: toggleable({ state: { inactive: interactive({ From bbb222b6fcaa791d6b4a57eef7ce8aa7553fcd1f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Aug 2023 20:56:26 +0300 Subject: [PATCH 140/142] Add a default binding for toggling inlay hints --- assets/keymaps/default.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1bd973e83b8a8bd193b963f50148a9e603f6640c..a1c771da02c156bbb389a1952ac85782225cb9eb 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -521,7 +521,8 @@ "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", - "cmd-alt-i": "zed::DebugElements" + "cmd-alt-i": "zed::DebugElements", + "ctrl-shift-:": "editor::ToggleInlayHints", } }, { From f0ab27a83da2459af999bb0e0f565ec702a92089 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 14:04:17 -0400 Subject: [PATCH 141/142] Reorder "Select All" button --- crates/search/src/buffer_search.rs | 2 +- styles/src/style_tree/search.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 26ae86f375a134e198299a55989c7cfc0cb71027..78729df936c15140d034ce29a6c4ccb108c46deb 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -261,8 +261,8 @@ impl View for BufferSearchBar { .with_height(theme.search.search_bar_row_height); let nav_column = Flex::row() - .with_child(Flex::row().with_children(match_count)) .with_child(self.render_action_button("all", cx)) + .with_child(Flex::row().with_children(match_count)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) .constrained() diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 73f35b9dd4b4a4148f93db0172e91864027819fe..c37a4e4b9a1bedec7c159c6f729dab0e9a882362 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -168,8 +168,8 @@ export default function search(): any { // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ state: { - inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING } }), - active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING } }) + inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }), + active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }) } }), editor, @@ -183,9 +183,8 @@ export default function search(): any { border: border(theme.highest, "negative"), }, match_index: { - ...text(theme.highest, "mono", "variant"), + ...text(theme.highest, "mono", { size: "sm" }), padding: { - left: 9, right: SEARCH_ROW_SPACING, }, }, From e89ccf2e2692ab6b0e33070e9caa180af5208177 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 14:09:31 -0400 Subject: [PATCH 142/142] Remove unused `label_button` --- styles/src/component/label_button.ts | 78 ---------------------------- 1 file changed, 78 deletions(-) delete mode 100644 styles/src/component/label_button.ts diff --git a/styles/src/component/label_button.ts b/styles/src/component/label_button.ts deleted file mode 100644 index 3f1c54a7f684198d00f30f294307b3d9d6b9d472..0000000000000000000000000000000000000000 --- a/styles/src/component/label_button.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Interactive, interactive, toggleable, Toggleable } from "../element" -import { TextStyle, background, text } from "../style_tree/components" -import { useTheme } from "../theme" -import { Button } from "./button" - -type LabelButtonStyle = { - corder_radius: number - background: string | null - padding: { - top: number - bottom: number - left: number - right: number - }, - margin: Button.Options['margin'] - button_height: number -} & TextStyle - -/** Styles an Interactive<ContainedText> */ -export function label_button_style( - options: Partial = { - variant: Button.variant.Default, - shape: Button.shape.Rectangle, - states: { - hovered: true, - pressed: true - } - } -): Interactive { - const theme = useTheme() - - const base = Button.button_base(options) - const layer = options.layer ?? theme.middle - const color = options.color ?? "base" - - const default_state = { - ...base, - ...text(layer ?? theme.lowest, "sans", color), - font_size: Button.FONT_SIZE, - } - - return interactive({ - base: default_state, - state: { - hovered: { - background: background(layer, options.background ?? color, "hovered") - }, - clicked: { - background: background(layer, options.background ?? color, "pressed") - } - } - }) -} - -/** Styles an Toggleable<Interactive<ContainedText>> */ -export function toggle_label_button_style( - options: Partial = { - variant: Button.variant.Default, - shape: Button.shape.Rectangle, - states: { - hovered: true, - pressed: true - } - } -): Toggleable> { - const activeOptions = { - ...options, - color: options.active_color || options.color, - background: options.active_background || options.background - } - - return toggleable({ - state: { - inactive: label_button_style(options), - active: label_button_style(activeOptions), - }, - }) -}