Merge branch 'main' into panic-hunting

Mikayla created

Change summary

.github/actions/run_tests/action.yml              |    6 
.github/workflows/ci.yml                          |    5 
.github/workflows/release_nightly.yml             |    2 
Cargo.lock                                        |    7 
crates/call2/Cargo.toml                           |    3 
crates/call2/src/call2.rs                         |  129 +
crates/collab/Cargo.toml                          |    2 
crates/collab2/src/tests/test_server.rs           |    1 
crates/editor/src/editor.rs                       |   51 
crates/editor2/src/editor.rs                      |  478 +++---
crates/editor2/src/element.rs                     | 1055 +++++++++-------
crates/gpui2/src/elements/text.rs                 |   21 
crates/gpui2/src/elements/uniform_list.rs         |   51 
crates/gpui2/src/style.rs                         |    9 
crates/gpui2/src/styled.rs                        |   20 
crates/language2/src/buffer.rs                    |   41 
crates/language2/src/proto.rs                     |    1 
crates/project2/src/lsp_command.rs                |   25 
crates/project_panel2/src/project_panel.rs        |    2 
crates/terminal_view2/src/terminal_view.rs        |    9 
crates/ui2/src/components/context_menu.rs         |   33 
crates/ui2/src/components/list.rs                 |   31 
crates/ui2/src/components/stories/context_menu.rs |   20 
crates/workspace2/Cargo.toml                      |    2 
crates/workspace2/src/dock.rs                     |   16 
crates/workspace2/src/pane_group.rs               |    7 
crates/workspace2/src/workspace2.rs               |  203 +-
crates/zed/Cargo.toml                             |    2 
crates/zed2/src/main.rs                           |    2 
29 files changed, 1,300 insertions(+), 934 deletions(-)

Detailed changes

.github/actions/run_tests/action.yml 🔗

@@ -19,16 +19,12 @@ runs:
 
     - name: Limit target directory size
       shell: bash -euxo pipefail {0}
-      run: script/clear-target-dir-if-larger-than 70
+      run: script/clear-target-dir-if-larger-than 100
 
     - name: Run check
-      env:
-        RUSTFLAGS: -D warnings
       shell: bash -euxo pipefail {0}
       run: cargo check --tests --workspace
 
     - name: Run tests
-      env:
-        RUSTFLAGS: -D warnings
       shell: bash -euxo pipefail {0}
       run: cargo nextest run --workspace --no-fail-fast

.github/workflows/ci.yml 🔗

@@ -23,6 +23,9 @@ jobs:
       - self-hosted
       - test
     steps:
+      - name: Set up default .cargo/config.toml
+        run: printf "[build]\nrustflags = [\"-D\", \"warnings\"]" > $HOME/.cargo/config.toml
+
       - name: Checkout repo
         uses: actions/checkout@v3
         with:
@@ -87,7 +90,7 @@ jobs:
           submodules: "recursive"
 
       - name: Limit target directory size
-        run: script/clear-target-dir-if-larger-than 70
+        run: script/clear-target-dir-if-larger-than 100
 
       - name: Determine version and release channel
         if: ${{ startsWith(github.ref, 'refs/tags/v') }}

.github/workflows/release_nightly.yml 🔗

@@ -79,7 +79,7 @@ jobs:
           submodules: "recursive"
 
       - name: Limit target directory size
-        run: script/clear-target-dir-if-larger-than 70
+        run: script/clear-target-dir-if-larger-than 100
 
       - name: Set release channel to nightly
         run: |

Cargo.lock 🔗

@@ -1186,6 +1186,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-broadcast",
+ "async-trait",
  "audio2",
  "client2",
  "collections",
@@ -1204,6 +1205,7 @@ dependencies = [
  "serde_json",
  "settings2",
  "util",
+ "workspace2",
 ]
 
 [[package]]
@@ -1664,7 +1666,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.28.0"
+version = "0.29.0"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -11381,6 +11383,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-recursion 1.0.5",
+ "async-trait",
  "bincode",
  "call2",
  "client2",
@@ -11493,7 +11496,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.114.0"
+version = "0.115.0"
 dependencies = [
  "activity_indicator",
  "ai",

crates/call2/Cargo.toml 🔗

@@ -31,7 +31,8 @@ media = { path = "../media" }
 project = { package = "project2", path = "../project2" }
 settings = { package = "settings2", path = "../settings2" }
 util = { path = "../util" }
-
+workspace = {package = "workspace2", path = "../workspace2"}
+async-trait.workspace = true
 anyhow.workspace = true
 async-broadcast = "0.4"
 futures.workspace = true

crates/call2/src/call2.rs 🔗

@@ -2,24 +2,29 @@ pub mod call_settings;
 pub mod participant;
 pub mod room;
 
-use anyhow::{anyhow, Result};
+use anyhow::{anyhow, bail, Result};
+use async_trait::async_trait;
 use audio::Audio;
 use call_settings::CallSettings;
-use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
+use client::{
+    proto::{self, PeerId},
+    Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE,
+};
 use collections::HashSet;
 use futures::{channel::oneshot, future::Shared, Future, FutureExt};
 use gpui::{
-    AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
-    WeakModel,
+    AppContext, AsyncAppContext, AsyncWindowContext, Context, EventEmitter, Model, ModelContext,
+    Subscription, Task, View, ViewContext, WeakModel, WeakView,
 };
+pub use participant::ParticipantLocation;
 use postage::watch;
 use project::Project;
 use room::Event;
+pub use room::Room;
 use settings::Settings;
 use std::sync::Arc;
-
-pub use participant::ParticipantLocation;
-pub use room::Room;
+use util::ResultExt;
+use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
 
 pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
     CallSettings::register(cx);
@@ -505,6 +510,116 @@ pub fn report_call_event_for_channel(
     )
 }
 
+pub struct Call {
+    active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
+    parent_workspace: WeakView<Workspace>,
+}
+
+impl Call {
+    pub fn new(
+        parent_workspace: WeakView<Workspace>,
+        cx: &mut ViewContext<'_, Workspace>,
+    ) -> Box<dyn CallHandler> {
+        let mut active_call = None;
+        if cx.has_global::<Model<ActiveCall>>() {
+            let call = cx.global::<Model<ActiveCall>>().clone();
+            let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
+            active_call = Some((call, subscriptions));
+        }
+        Box::new(Self {
+            active_call,
+            parent_workspace,
+        })
+    }
+    fn on_active_call_event(
+        workspace: &mut Workspace,
+        _: Model<ActiveCall>,
+        event: &room::Event,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        match event {
+            room::Event::ParticipantLocationChanged { participant_id }
+            | room::Event::RemoteVideoTracksChanged { participant_id } => {
+                workspace.leader_updated(*participant_id, cx);
+            }
+            _ => {}
+        }
+    }
+}
+
+#[async_trait(?Send)]
+impl CallHandler for Call {
+    fn shared_screen_for_peer(
+        &self,
+        peer_id: PeerId,
+        _pane: &View<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Box<dyn ItemHandle>> {
+        let (call, _) = self.active_call.as_ref()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participant_for_peer_id(peer_id)?;
+        let _track = participant.video_tracks.values().next()?.clone();
+        let _user = participant.user.clone();
+        todo!();
+        // for item in pane.read(cx).items_of_type::<SharedScreen>() {
+        //     if item.read(cx).peer_id == peer_id {
+        //         return Box::new(Some(item));
+        //     }
+        // }
+
+        // Some(Box::new(cx.build_view(|cx| {
+        //     SharedScreen::new(&track, peer_id, user.clone(), cx)
+        // })))
+    }
+
+    fn room_id(&self, cx: &AppContext) -> Option<u64> {
+        Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
+    }
+    fn hang_up(&self, mut cx: AsyncWindowContext) -> Result<Task<Result<()>>> {
+        let Some((call, _)) = self.active_call.as_ref() else {
+            bail!("Cannot exit a call; not in a call");
+        };
+
+        call.update(&mut cx, |this, cx| this.hang_up(cx))
+    }
+    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
+        ActiveCall::global(cx).read(cx).location().cloned()
+    }
+    fn peer_state(
+        &mut self,
+        leader_id: PeerId,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<(bool, bool)> {
+        let (call, _) = self.active_call.as_ref()?;
+        let room = call.read(cx).room()?.read(cx);
+        let participant = room.remote_participant_for_peer_id(leader_id)?;
+
+        let leader_in_this_app;
+        let leader_in_this_project;
+        match participant.location {
+            ParticipantLocation::SharedProject { project_id } => {
+                leader_in_this_app = true;
+                leader_in_this_project = Some(project_id)
+                    == self
+                        .parent_workspace
+                        .update(cx, |this, cx| this.project().read(cx).remote_id())
+                        .log_err()
+                        .flatten();
+            }
+            ParticipantLocation::UnsharedProject => {
+                leader_in_this_app = true;
+                leader_in_this_project = false;
+            }
+            ParticipantLocation::External => {
+                leader_in_this_app = false;
+                leader_in_this_project = false;
+            }
+        };
+
+        Some((leader_in_this_project, leader_in_this_app))
+    }
+}
+
 #[cfg(test)]
 mod test {
     use gpui::TestAppContext;

crates/collab/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.28.0"
+version = "0.29.0"
 publish = false
 
 [[bin]]

crates/collab2/src/tests/test_server.rs 🔗

@@ -221,6 +221,7 @@ impl TestServer {
             fs: fs.clone(),
             build_window_options: |_, _, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),
+            call_factory: |_, _| Box::new(workspace::TestCallHandler),
         });
 
         cx.update(|cx| {

crates/editor/src/editor.rs 🔗

@@ -1001,17 +1001,18 @@ impl CompletionsMenu {
 
     fn pre_resolve_completion_documentation(
         &self,
-        project: Option<ModelHandle<Project>>,
+        editor: &Editor,
         cx: &mut ViewContext<Editor>,
-    ) {
+    ) -> Option<Task<()>> {
         let settings = settings::get::<EditorSettings>(cx);
         if !settings.show_completion_documentation {
-            return;
+            return None;
         }
 
-        let Some(project) = project else {
-            return;
+        let Some(project) = editor.project.clone() else {
+            return None;
         };
+
         let client = project.read(cx).client();
         let language_registry = project.read(cx).languages().clone();
 
@@ -1021,7 +1022,7 @@ impl CompletionsMenu {
         let completions = self.completions.clone();
         let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
 
-        cx.spawn(move |this, mut cx| async move {
+        Some(cx.spawn(move |this, mut cx| async move {
             if is_remote {
                 let Some(project_id) = project_id else {
                     log::error!("Remote project without remote_id");
@@ -1083,8 +1084,7 @@ impl CompletionsMenu {
                     _ = this.update(&mut cx, |_, cx| cx.notify());
                 }
             }
-        })
-        .detach();
+        }))
     }
 
     fn attempt_resolve_selected_completion_documentation(
@@ -3580,7 +3580,8 @@ impl Editor {
         let id = post_inc(&mut self.next_completion_id);
         let task = cx.spawn(|this, mut cx| {
             async move {
-                let menu = if let Some(completions) = completions.await.log_err() {
+                let completions = completions.await.log_err();
+                let (menu, pre_resolve_task) = if let Some(completions) = completions {
                     let mut menu = CompletionsMenu {
                         id,
                         initial_position: position,
@@ -3601,21 +3602,26 @@ impl Editor {
                         selected_item: 0,
                         list: Default::default(),
                     };
+
                     menu.filter(query.as_deref(), cx.background()).await;
+
                     if menu.matches.is_empty() {
-                        None
+                        (None, None)
                     } else {
-                        _ = this.update(&mut cx, |editor, cx| {
-                            menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
-                        });
-                        Some(menu)
+                        let pre_resolve_task = this
+                            .update(&mut cx, |editor, cx| {
+                                menu.pre_resolve_completion_documentation(editor, cx)
+                            })
+                            .ok()
+                            .flatten();
+                        (Some(menu), pre_resolve_task)
                     }
                 } else {
-                    None
+                    (None, None)
                 };
 
                 this.update(&mut cx, |this, cx| {
-                    this.completion_tasks.retain(|(task_id, _)| *task_id > id);
+                    this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
 
                     let mut context_menu = this.context_menu.write();
                     match context_menu.as_ref() {
@@ -3636,10 +3642,10 @@ impl Editor {
                         drop(context_menu);
                         this.discard_copilot_suggestion(cx);
                         cx.notify();
-                    } else if this.completion_tasks.is_empty() {
-                        // If there are no more completion tasks and the last menu was
-                        // empty, we should hide it. If it was already hidden, we should
-                        // also show the copilot suggestion when available.
+                    } else if this.completion_tasks.len() <= 1 {
+                        // If there are no more completion tasks (omitting ourself) and
+                        // the last menu was empty, we should hide it. If it was already
+                        // hidden, we should also show the copilot suggestion when available.
                         drop(context_menu);
                         if this.hide_context_menu(cx).is_none() {
                             this.update_visible_copilot_suggestion(cx);
@@ -3647,10 +3653,15 @@ impl Editor {
                     }
                 })?;
 
+                if let Some(pre_resolve_task) = pre_resolve_task {
+                    pre_resolve_task.await;
+                }
+
                 Ok::<_, anyhow::Error>(())
             }
             .log_err()
         });
+
         self.completion_tasks.push((id, task));
     }
 

crates/editor2/src/editor.rs 🔗

@@ -44,7 +44,7 @@ use gpui::{
     EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
     Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render,
     SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
-    ViewContext, VisualContext, WeakView, WindowContext,
+    ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -54,13 +54,13 @@ use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
     language_settings::{self, all_language_settings, InlayHintSettings},
-    point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape,
-    Diagnostic, IndentKind, IndentSize, Language, LanguageRegistry, LanguageServerName,
-    OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
+    point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion,
+    CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, LanguageRegistry,
+    LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
 };
 use lazy_static::lazy_static;
 use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
-use lsp::{DiagnosticSeverity, Documentation, LanguageServerId};
+use lsp::{DiagnosticSeverity, LanguageServerId};
 use movement::TextLayoutDetails;
 use multi_buffer::ToOffsetUtf16;
 pub use multi_buffer::{
@@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope};
 use theme::{
     ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
 };
-use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
+use ui::{h_stack, v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
 use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
 use workspace::{
     item::{ItemEvent, ItemHandle},
@@ -1224,207 +1224,201 @@ impl CompletionsMenu {
         workspace: Option<WeakView<Workspace>>,
         cx: &mut ViewContext<Editor>,
     ) -> AnyElement {
-        todo!("old implementation below")
-    }
+        let settings = EditorSettings::get_global(cx);
+        let show_completion_documentation = settings.show_completion_documentation;
 
-    //     enum CompletionTag {}
+        let widest_completion_ix = self
+            .matches
+            .iter()
+            .enumerate()
+            .max_by_key(|(_, mat)| {
+                let completions = self.completions.read();
+                let completion = &completions[mat.candidate_id];
+                let documentation = &completion.documentation;
+
+                let mut len = completion.label.text.chars().count();
+                if let Some(Documentation::SingleLine(text)) = documentation {
+                    if show_completion_documentation {
+                        len += text.chars().count();
+                    }
+                }
 
-    //     let settings = EditorSettings>(cx);
-    //     let show_completion_documentation = settings.show_completion_documentation;
+                len
+            })
+            .map(|(ix, _)| ix);
 
-    //     let widest_completion_ix = self
-    //         .matches
-    //         .iter()
-    //         .enumerate()
-    //         .max_by_key(|(_, mat)| {
-    //             let completions = self.completions.read();
-    //             let completion = &completions[mat.candidate_id];
-    //             let documentation = &completion.documentation;
+        let completions = self.completions.clone();
+        let matches = self.matches.clone();
+        let selected_item = self.selected_item;
 
-    //             let mut len = completion.label.text.chars().count();
-    //             if let Some(Documentation::SingleLine(text)) = documentation {
-    //                 if show_completion_documentation {
-    //                     len += text.chars().count();
-    //                 }
-    //             }
+        let list = uniform_list(
+            cx.view().clone(),
+            "completions",
+            matches.len(),
+            move |editor, range, cx| {
+                let start_ix = range.start;
+                let completions_guard = completions.read();
 
-    //             len
-    //         })
-    //         .map(|(ix, _)| ix);
+                matches[range]
+                    .iter()
+                    .enumerate()
+                    .map(|(ix, mat)| {
+                        let item_ix = start_ix + ix;
+                        let candidate_id = mat.candidate_id;
+                        let completion = &completions_guard[candidate_id];
 
-    //     let completions = self.completions.clone();
-    //     let matches = self.matches.clone();
-    //     let selected_item = self.selected_item;
-
-    //     let list = UniformList::new(self.list.clone(), matches.len(), cx, {
-    //         let style = style.clone();
-    //         move |_, range, items, cx| {
-    //             let start_ix = range.start;
-    //             let completions_guard = completions.read();
-
-    //             for (ix, mat) in matches[range].iter().enumerate() {
-    //                 let item_ix = start_ix + ix;
-    //                 let candidate_id = mat.candidate_id;
-    //                 let completion = &completions_guard[candidate_id];
-
-    //                 let documentation = if show_completion_documentation {
-    //                     &completion.documentation
-    //                 } else {
-    //                     &None
-    //                 };
+                        let documentation = if show_completion_documentation {
+                            &completion.documentation
+                        } else {
+                            &None
+                        };
 
-    //                 items.push(
-    //                     MouseEventHandler::new::<CompletionTag, _>(
-    //                         mat.candidate_id,
-    //                         cx,
-    //                         |state, _| {
-    //                             let item_style = if item_ix == selected_item {
-    //                                 style.autocomplete.selected_item
-    //                             } else if state.hovered() {
-    //                                 style.autocomplete.hovered_item
-    //                             } else {
-    //                                 style.autocomplete.item
-    //                             };
-
-    //                             let completion_label =
-    //                                 Text::new(completion.label.text.clone(), style.text.clone())
-    //                                     .with_soft_wrap(false)
-    //                                     .with_highlights(
-    //                                         combine_syntax_and_fuzzy_match_highlights(
-    //                                             &completion.label.text,
-    //                                             style.text.color.into(),
-    //                                             styled_runs_for_code_label(
-    //                                                 &completion.label,
-    //                                                 &style.syntax,
-    //                                             ),
-    //                                             &mat.positions,
-    //                                         ),
-    //                                     );
-
-    //                             if let Some(Documentation::SingleLine(text)) = documentation {
-    //                                 Flex::row()
-    //                                     .with_child(completion_label)
-    //                                     .with_children((|| {
-    //                                         let text_style = TextStyle {
-    //                                             color: style.autocomplete.inline_docs_color,
-    //                                             font_size: style.text.font_size
-    //                                                 * style.autocomplete.inline_docs_size_percent,
-    //                                             ..style.text.clone()
-    //                                         };
-
-    //                                         let label = Text::new(text.clone(), text_style)
-    //                                             .aligned()
-    //                                             .constrained()
-    //                                             .dynamically(move |constraint, _, _| {
-    //                                                 gpui::SizeConstraint {
-    //                                                     min: constraint.min,
-    //                                                     max: vec2f(
-    //                                                         constraint.max.x(),
-    //                                                         constraint.min.y(),
-    //                                                     ),
-    //                                                 }
-    //                                             });
-
-    //                                         if Some(item_ix) == widest_completion_ix {
-    //                                             Some(
-    //                                                 label
-    //                                                     .contained()
-    //                                                     .with_style(
-    //                                                         style
-    //                                                             .autocomplete
-    //                                                             .inline_docs_container,
-    //                                                     )
-    //                                                     .into_any(),
-    //                                             )
-    //                                         } else {
-    //                                             Some(label.flex_float().into_any())
-    //                                         }
-    //                                     })())
-    //                                     .into_any()
-    //                             } else {
-    //                                 completion_label.into_any()
-    //                             }
-    //                             .contained()
-    //                             .with_style(item_style)
-    //                             .constrained()
-    //                             .dynamically(
-    //                                 move |constraint, _, _| {
-    //                                     if Some(item_ix) == widest_completion_ix {
-    //                                         constraint
-    //                                     } else {
-    //                                         gpui::SizeConstraint {
-    //                                             min: constraint.min,
-    //                                             max: constraint.min,
-    //                                         }
-    //                                     }
-    //                                 },
-    //                             )
-    //                         },
-    //                     )
-    //                     .with_cursor_style(CursorStyle::PointingHand)
-    //                     .on_down(MouseButton::Left, move |_, this, cx| {
-    //                         this.confirm_completion(
-    //                             &ConfirmCompletion {
-    //                                 item_ix: Some(item_ix),
-    //                             },
-    //                             cx,
-    //                         )
-    //                         .map(|task| task.detach());
-    //                     })
-    //                     .constrained()
-    //                     .with_min_width(style.autocomplete.completion_min_width)
-    //                     .with_max_width(style.autocomplete.completion_max_width)
-    //                     .into_any(),
-    //                 );
-    //             }
-    //         }
-    //     })
-    //     .with_width_from_item(widest_completion_ix);
-
-    //     enum MultiLineDocumentation {}
-
-    //     Flex::row()
-    //         .with_child(list.flex(1., false))
-    //         .with_children({
-    //             let mat = &self.matches[selected_item];
-    //             let completions = self.completions.read();
-    //             let completion = &completions[mat.candidate_id];
-    //             let documentation = &completion.documentation;
-
-    //             match documentation {
-    //                 Some(Documentation::MultiLinePlainText(text)) => Some(
-    //                     Flex::column()
-    //                         .scrollable::<MultiLineDocumentation>(0, None, cx)
-    //                         .with_child(
-    //                             Text::new(text.clone(), style.text.clone()).with_soft_wrap(true),
-    //                         )
-    //                         .contained()
-    //                         .with_style(style.autocomplete.alongside_docs_container)
-    //                         .constrained()
-    //                         .with_max_width(style.autocomplete.alongside_docs_max_width)
-    //                         .flex(1., false),
-    //                 ),
-
-    //                 Some(Documentation::MultiLineMarkdown(parsed)) => Some(
-    //                     Flex::column()
-    //                         .scrollable::<MultiLineDocumentation>(0, None, cx)
-    //                         .with_child(render_parsed_markdown::<MultiLineDocumentation>(
-    //                             parsed, &style, workspace, cx,
-    //                         ))
-    //                         .contained()
-    //                         .with_style(style.autocomplete.alongside_docs_container)
-    //                         .constrained()
-    //                         .with_max_width(style.autocomplete.alongside_docs_max_width)
-    //                         .flex(1., false),
-    //                 ),
-
-    //                 _ => None,
-    //             }
-    //         })
-    //         .contained()
-    //         .with_style(style.autocomplete.container)
-    //         .into_any()
-    // }
+                        // todo!("highlights")
+                        // let highlights = combine_syntax_and_fuzzy_match_highlights(
+                        //     &completion.label.text,
+                        //     style.text.color.into(),
+                        //     styled_runs_for_code_label(&completion.label, &style.syntax),
+                        //     &mat.positions,
+                        // )
+
+                        // todo!("documentation")
+                        // MouseEventHandler::new::<CompletionTag, _>(mat.candidate_id, cx, |state, _| {
+                        //     let completion_label = HighlightedLabel::new(
+                        //         completion.label.text.clone(),
+                        //         combine_syntax_and_fuzzy_match_highlights(
+                        //             &completion.label.text,
+                        //             style.text.color.into(),
+                        //             styled_runs_for_code_label(&completion.label, &style.syntax),
+                        //             &mat.positions,
+                        //         ),
+                        //     );
+                        //     Text::new(completion.label.text.clone(), style.text.clone())
+                        //         .with_soft_wrap(false)
+                        //         .with_highlights();
+
+                        //     if let Some(Documentation::SingleLine(text)) = documentation {
+                        //         h_stack()
+                        //             .child(completion_label)
+                        //             .with_children((|| {
+                        //                 let text_style = TextStyle {
+                        //                     color: style.autocomplete.inline_docs_color,
+                        //                     font_size: style.text.font_size
+                        //                         * style.autocomplete.inline_docs_size_percent,
+                        //                     ..style.text.clone()
+                        //                 };
+
+                        //                 let label = Text::new(text.clone(), text_style)
+                        //                     .aligned()
+                        //                     .constrained()
+                        //                     .dynamically(move |constraint, _, _| gpui::SizeConstraint {
+                        //                         min: constraint.min,
+                        //                         max: vec2f(constraint.max.x(), constraint.min.y()),
+                        //                     });
+
+                        //                 if Some(item_ix) == widest_completion_ix {
+                        //                     Some(
+                        //                         label
+                        //                             .contained()
+                        //                             .with_style(style.autocomplete.inline_docs_container)
+                        //                             .into_any(),
+                        //                     )
+                        //                 } else {
+                        //                     Some(label.flex_float().into_any())
+                        //                 }
+                        //             })())
+                        //             .into_any()
+                        //     } else {
+                        //         completion_label.into_any()
+                        //     }
+                        //     .contained()
+                        //     .with_style(item_style)
+                        //     .constrained()
+                        //     .dynamically(move |constraint, _, _| {
+                        //         if Some(item_ix) == widest_completion_ix {
+                        //             constraint
+                        //         } else {
+                        //             gpui::SizeConstraint {
+                        //                 min: constraint.min,
+                        //                 max: constraint.min,
+                        //             }
+                        //         }
+                        //     })
+                        // })
+                        // .with_cursor_style(CursorStyle::PointingHand)
+                        // .on_down(MouseButton::Left, move |_, this, cx| {
+                        //     this.confirm_completion(
+                        //         &ConfirmCompletion {
+                        //             item_ix: Some(item_ix),
+                        //         },
+                        //         cx,
+                        //     )
+                        //     .map(|task| task.detach());
+                        // })
+                        // .constrained()
+                        //
+                        div()
+                            .id(mat.candidate_id)
+                            .whitespace_nowrap()
+                            .overflow_hidden()
+                            .bg(gpui::green())
+                            .hover(|style| style.bg(gpui::blue()))
+                            .when(item_ix == selected_item, |div| div.bg(gpui::red()))
+                            .child(SharedString::from(completion.label.text.clone()))
+                            .min_w(px(300.))
+                            .max_w(px(700.))
+                    })
+                    .collect()
+            },
+        )
+        .track_scroll(self.scroll_handle.clone())
+        .with_width_from_item(widest_completion_ix);
+
+        list.render_into_any()
+        // todo!("multiline documentation")
+        //     enum MultiLineDocumentation {}
+
+        //     Flex::row()
+        //         .with_child(list.flex(1., false))
+        //         .with_children({
+        //             let mat = &self.matches[selected_item];
+        //             let completions = self.completions.read();
+        //             let completion = &completions[mat.candidate_id];
+        //             let documentation = &completion.documentation;
+
+        //             match documentation {
+        //                 Some(Documentation::MultiLinePlainText(text)) => Some(
+        //                     Flex::column()
+        //                         .scrollable::<MultiLineDocumentation>(0, None, cx)
+        //                         .with_child(
+        //                             Text::new(text.clone(), style.text.clone()).with_soft_wrap(true),
+        //                         )
+        //                         .contained()
+        //                         .with_style(style.autocomplete.alongside_docs_container)
+        //                         .constrained()
+        //                         .with_max_width(style.autocomplete.alongside_docs_max_width)
+        //                         .flex(1., false),
+        //                 ),
+
+        //                 Some(Documentation::MultiLineMarkdown(parsed)) => Some(
+        //                     Flex::column()
+        //                         .scrollable::<MultiLineDocumentation>(0, None, cx)
+        //                         .with_child(render_parsed_markdown::<MultiLineDocumentation>(
+        //                             parsed, &style, workspace, cx,
+        //                         ))
+        //                         .contained()
+        //                         .with_style(style.autocomplete.alongside_docs_container)
+        //                         .constrained()
+        //                         .with_max_width(style.autocomplete.alongside_docs_max_width)
+        //                         .flex(1., false),
+        //                 ),
+
+        //                 _ => None,
+        //             }
+        //         })
+        //         .contained()
+        //         .with_style(style.autocomplete.container)
+        //         .into_any()
+    }
 
     pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
         let mut matches = if let Some(query) = query {
@@ -1594,6 +1588,7 @@ impl CodeActionsMenu {
         .elevation_1(cx)
         .px_2()
         .py_1()
+        .track_scroll(self.scroll_handle.clone())
         .with_width_from_item(
             self.actions
                 .iter()
@@ -9405,6 +9400,7 @@ impl Render for Editor {
                 font_style: FontStyle::Normal,
                 line_height: relative(1.).into(),
                 underline: None,
+                white_space: WhiteSpace::Normal,
             },
 
             EditorMode::AutoHeight { max_lines } => todo!(),
@@ -9418,6 +9414,7 @@ impl Render for Editor {
                 font_style: FontStyle::Normal,
                 line_height: relative(settings.buffer_line_height.value()),
                 underline: None,
+                white_space: WhiteSpace::Normal,
             },
         };
 
@@ -10126,49 +10123,50 @@ pub fn combine_syntax_and_fuzzy_match_highlights(
     result
 }
 
-// pub fn styled_runs_for_code_label<'a>(
-//     label: &'a CodeLabel,
-//     syntax_theme: &'a theme::SyntaxTheme,
-// ) -> impl 'a + Iterator<Item = (Range<usize>, HighlightStyle)> {
-//     let fade_out = HighlightStyle {
-//         fade_out: Some(0.35),
-//         ..Default::default()
-//     };
+pub fn styled_runs_for_code_label<'a>(
+    label: &'a CodeLabel,
+    syntax_theme: &'a theme::SyntaxTheme,
+) -> impl 'a + Iterator<Item = (Range<usize>, HighlightStyle)> {
+    let fade_out = HighlightStyle {
+        fade_out: Some(0.35),
+        ..Default::default()
+    };
+
+    let mut prev_end = label.filter_range.end;
+    label
+        .runs
+        .iter()
+        .enumerate()
+        .flat_map(move |(ix, (range, highlight_id))| {
+            let style = if let Some(style) = highlight_id.style(syntax_theme) {
+                style
+            } else {
+                return Default::default();
+            };
+            let mut muted_style = style;
+            muted_style.highlight(fade_out);
 
-//     let mut prev_end = label.filter_range.end;
-//     label
-//         .runs
-//         .iter()
-//         .enumerate()
-//         .flat_map(move |(ix, (range, highlight_id))| {
-//             let style = if let Some(style) = highlight_id.style(syntax_theme) {
-//                 style
-//             } else {
-//                 return Default::default();
-//             };
-//             let mut muted_style = style;
-//             muted_style.highlight(fade_out);
-
-//             let mut runs = SmallVec::<[(Range<usize>, HighlightStyle); 3]>::new();
-//             if range.start >= label.filter_range.end {
-//                 if range.start > prev_end {
-//                     runs.push((prev_end..range.start, fade_out));
-//                 }
-//                 runs.push((range.clone(), muted_style));
-//             } else if range.end <= label.filter_range.end {
-//                 runs.push((range.clone(), style));
-//             } else {
-//                 runs.push((range.start..label.filter_range.end, style));
-//                 runs.push((label.filter_range.end..range.end, muted_style));
-//             }
-//             prev_end = cmp::max(prev_end, range.end);
+            let mut runs = SmallVec::<[(Range<usize>, HighlightStyle); 3]>::new();
+            if range.start >= label.filter_range.end {
+                if range.start > prev_end {
+                    runs.push((prev_end..range.start, fade_out));
+                }
+                runs.push((range.clone(), muted_style));
+            } else if range.end <= label.filter_range.end {
+                runs.push((range.clone(), style));
+            } else {
+                runs.push((range.start..label.filter_range.end, style));
+                runs.push((label.filter_range.end..range.end, muted_style));
+            }
+            prev_end = cmp::max(prev_end, range.end);
 
-//             if ix + 1 == label.runs.len() && label.text.len() > prev_end {
-//                 runs.push((prev_end..label.text.len(), fade_out));
-//             }
+            if ix + 1 == label.runs.len() && label.text.len() > prev_end {
+                runs.push((prev_end..label.text.len(), fade_out));
+            }
 
-//             runs
-//         })
+            runs
+        })
+}
 
 pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
     let mut index = 0;

crates/editor2/src/element.rs 🔗

@@ -124,6 +124,180 @@ impl EditorElement {
         }
     }
 
+    fn register_actions(&self, cx: &mut WindowContext) {
+        let view = &self.editor;
+        register_action(view, cx, Editor::move_left);
+        register_action(view, cx, Editor::move_right);
+        register_action(view, cx, Editor::move_down);
+        register_action(view, cx, Editor::move_up);
+        // on_action(cx, Editor::new_file); todo!()
+        // on_action(cx, Editor::new_file_in_direction); todo!()
+        register_action(view, cx, Editor::cancel);
+        register_action(view, cx, Editor::newline);
+        register_action(view, cx, Editor::newline_above);
+        register_action(view, cx, Editor::newline_below);
+        register_action(view, cx, Editor::backspace);
+        register_action(view, cx, Editor::delete);
+        register_action(view, cx, Editor::tab);
+        register_action(view, cx, Editor::tab_prev);
+        register_action(view, cx, Editor::indent);
+        register_action(view, cx, Editor::outdent);
+        register_action(view, cx, Editor::delete_line);
+        register_action(view, cx, Editor::join_lines);
+        register_action(view, cx, Editor::sort_lines_case_sensitive);
+        register_action(view, cx, Editor::sort_lines_case_insensitive);
+        register_action(view, cx, Editor::reverse_lines);
+        register_action(view, cx, Editor::shuffle_lines);
+        register_action(view, cx, Editor::convert_to_upper_case);
+        register_action(view, cx, Editor::convert_to_lower_case);
+        register_action(view, cx, Editor::convert_to_title_case);
+        register_action(view, cx, Editor::convert_to_snake_case);
+        register_action(view, cx, Editor::convert_to_kebab_case);
+        register_action(view, cx, Editor::convert_to_upper_camel_case);
+        register_action(view, cx, Editor::convert_to_lower_camel_case);
+        register_action(view, cx, Editor::delete_to_previous_word_start);
+        register_action(view, cx, Editor::delete_to_previous_subword_start);
+        register_action(view, cx, Editor::delete_to_next_word_end);
+        register_action(view, cx, Editor::delete_to_next_subword_end);
+        register_action(view, cx, Editor::delete_to_beginning_of_line);
+        register_action(view, cx, Editor::delete_to_end_of_line);
+        register_action(view, cx, Editor::cut_to_end_of_line);
+        register_action(view, cx, Editor::duplicate_line);
+        register_action(view, cx, Editor::move_line_up);
+        register_action(view, cx, Editor::move_line_down);
+        register_action(view, cx, Editor::transpose);
+        register_action(view, cx, Editor::cut);
+        register_action(view, cx, Editor::copy);
+        register_action(view, cx, Editor::paste);
+        register_action(view, cx, Editor::undo);
+        register_action(view, cx, Editor::redo);
+        register_action(view, cx, Editor::move_page_up);
+        register_action(view, cx, Editor::move_page_down);
+        register_action(view, cx, Editor::next_screen);
+        register_action(view, cx, Editor::scroll_cursor_top);
+        register_action(view, cx, Editor::scroll_cursor_center);
+        register_action(view, cx, Editor::scroll_cursor_bottom);
+        register_action(view, cx, |editor, _: &LineDown, cx| {
+            editor.scroll_screen(&ScrollAmount::Line(1.), cx)
+        });
+        register_action(view, cx, |editor, _: &LineUp, cx| {
+            editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
+        });
+        register_action(view, cx, |editor, _: &HalfPageDown, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
+        });
+        register_action(view, cx, |editor, _: &HalfPageUp, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
+        });
+        register_action(view, cx, |editor, _: &PageDown, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(1.), cx)
+        });
+        register_action(view, cx, |editor, _: &PageUp, cx| {
+            editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
+        });
+        register_action(view, cx, Editor::move_to_previous_word_start);
+        register_action(view, cx, Editor::move_to_previous_subword_start);
+        register_action(view, cx, Editor::move_to_next_word_end);
+        register_action(view, cx, Editor::move_to_next_subword_end);
+        register_action(view, cx, Editor::move_to_beginning_of_line);
+        register_action(view, cx, Editor::move_to_end_of_line);
+        register_action(view, cx, Editor::move_to_start_of_paragraph);
+        register_action(view, cx, Editor::move_to_end_of_paragraph);
+        register_action(view, cx, Editor::move_to_beginning);
+        register_action(view, cx, Editor::move_to_end);
+        register_action(view, cx, Editor::select_up);
+        register_action(view, cx, Editor::select_down);
+        register_action(view, cx, Editor::select_left);
+        register_action(view, cx, Editor::select_right);
+        register_action(view, cx, Editor::select_to_previous_word_start);
+        register_action(view, cx, Editor::select_to_previous_subword_start);
+        register_action(view, cx, Editor::select_to_next_word_end);
+        register_action(view, cx, Editor::select_to_next_subword_end);
+        register_action(view, cx, Editor::select_to_beginning_of_line);
+        register_action(view, cx, Editor::select_to_end_of_line);
+        register_action(view, cx, Editor::select_to_start_of_paragraph);
+        register_action(view, cx, Editor::select_to_end_of_paragraph);
+        register_action(view, cx, Editor::select_to_beginning);
+        register_action(view, cx, Editor::select_to_end);
+        register_action(view, cx, Editor::select_all);
+        register_action(view, cx, |editor, action, cx| {
+            editor.select_all_matches(action, cx).log_err();
+        });
+        register_action(view, cx, Editor::select_line);
+        register_action(view, cx, Editor::split_selection_into_lines);
+        register_action(view, cx, Editor::add_selection_above);
+        register_action(view, cx, Editor::add_selection_below);
+        register_action(view, cx, |editor, action, cx| {
+            editor.select_next(action, cx).log_err();
+        });
+        register_action(view, cx, |editor, action, cx| {
+            editor.select_previous(action, cx).log_err();
+        });
+        register_action(view, cx, Editor::toggle_comments);
+        register_action(view, cx, Editor::select_larger_syntax_node);
+        register_action(view, cx, Editor::select_smaller_syntax_node);
+        register_action(view, cx, Editor::move_to_enclosing_bracket);
+        register_action(view, cx, Editor::undo_selection);
+        register_action(view, cx, Editor::redo_selection);
+        register_action(view, cx, Editor::go_to_diagnostic);
+        register_action(view, cx, Editor::go_to_prev_diagnostic);
+        register_action(view, cx, Editor::go_to_hunk);
+        register_action(view, cx, Editor::go_to_prev_hunk);
+        register_action(view, cx, Editor::go_to_definition);
+        register_action(view, cx, Editor::go_to_definition_split);
+        register_action(view, cx, Editor::go_to_type_definition);
+        register_action(view, cx, Editor::go_to_type_definition_split);
+        register_action(view, cx, Editor::fold);
+        register_action(view, cx, Editor::fold_at);
+        register_action(view, cx, Editor::unfold_lines);
+        register_action(view, cx, Editor::unfold_at);
+        register_action(view, cx, Editor::fold_selected_ranges);
+        register_action(view, cx, Editor::show_completions);
+        register_action(view, cx, Editor::toggle_code_actions);
+        // on_action(cx, Editor::open_excerpts); todo!()
+        register_action(view, cx, Editor::toggle_soft_wrap);
+        register_action(view, cx, Editor::toggle_inlay_hints);
+        register_action(view, cx, Editor::reveal_in_finder);
+        register_action(view, cx, Editor::copy_path);
+        register_action(view, cx, Editor::copy_relative_path);
+        register_action(view, cx, Editor::copy_highlight_json);
+        register_action(view, cx, |editor, action, cx| {
+            editor
+                .format(action, cx)
+                .map(|task| task.detach_and_log_err(cx));
+        });
+        register_action(view, cx, Editor::restart_language_server);
+        register_action(view, cx, Editor::show_character_palette);
+        // on_action(cx, Editor::confirm_completion); todo!()
+        register_action(view, cx, |editor, action, cx| {
+            editor
+                .confirm_code_action(action, cx)
+                .map(|task| task.detach_and_log_err(cx));
+        });
+        register_action(view, cx, |editor, action, cx| {
+            editor
+                .rename(action, cx)
+                .map(|task| task.detach_and_log_err(cx));
+        });
+        register_action(view, cx, |editor, action, cx| {
+            editor
+                .confirm_rename(action, cx)
+                .map(|task| task.detach_and_log_err(cx));
+        });
+        register_action(view, cx, |editor, action, cx| {
+            editor
+                .find_all_references(action, cx)
+                .map(|task| task.detach_and_log_err(cx));
+        });
+        register_action(view, cx, Editor::next_copilot_suggestion);
+        register_action(view, cx, Editor::previous_copilot_suggestion);
+        register_action(view, cx, Editor::copilot_suggest);
+        register_action(view, cx, Editor::context_menu_first);
+        register_action(view, cx, Editor::context_menu_prev);
+        register_action(view, cx, Editor::context_menu_next);
+        register_action(view, cx, Editor::context_menu_last);
+    }
+
     fn mouse_down(
         editor: &mut Editor,
         event: &MouseDownEvent,
@@ -459,7 +633,6 @@ impl EditorElement {
         &mut self,
         bounds: Bounds<Pixels>,
         layout: &mut LayoutState,
-        editor: &mut Editor,
         cx: &mut WindowContext,
     ) {
         let line_height = layout.position_map.line_height;
@@ -616,14 +789,19 @@ impl EditorElement {
         &mut self,
         text_bounds: Bounds<Pixels>,
         layout: &mut LayoutState,
-        editor: &mut Editor,
         cx: &mut WindowContext,
     ) {
         let scroll_position = layout.position_map.snapshot.scroll_position();
         let start_row = layout.visible_display_row_range.start;
         let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO);
         let line_end_overshoot = 0.15 * layout.position_map.line_height;
-        let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
+        let whitespace_setting = self
+            .editor
+            .read(cx)
+            .buffer
+            .read(cx)
+            .settings_at(0, cx)
+            .show_whitespaces;
 
         cx.with_content_mask(
             Some(ContentMask {
@@ -748,7 +926,7 @@ impl EditorElement {
                             invisible_display_ranges.push(selection.range.clone());
                         }
 
-                        if !selection.is_local || editor.show_local_cursors(cx) {
+                        if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) {
                             let cursor_position = selection.head;
                             if layout
                                 .visible_display_row_range
@@ -800,12 +978,14 @@ impl EditorElement {
                                     * layout.position_map.line_height
                                     - layout.position_map.scroll_position.y;
                                 if selection.is_newest {
-                                    editor.pixel_position_of_newest_cursor = Some(point(
-                                        text_bounds.origin.x + x + block_width / 2.,
-                                        text_bounds.origin.y
-                                            + y
-                                            + layout.position_map.line_height / 2.,
-                                    ));
+                                    self.editor.update(cx, |editor, _| {
+                                        editor.pixel_position_of_newest_cursor = Some(point(
+                                            text_bounds.origin.x + x + block_width / 2.,
+                                            text_bounds.origin.y
+                                                + y
+                                                + layout.position_map.line_height / 2.,
+                                        ))
+                                    });
                                 }
                                 cursors.push(Cursor {
                                     color: selection_style.cursor,
@@ -871,7 +1051,7 @@ impl EditorElement {
                         }
 
                         if list_origin.y + list_height > text_bounds.lower_right().y {
-                            list_origin.y -= layout.position_map.line_height - list_height;
+                            list_origin.y -= layout.position_map.line_height + list_height;
                         }
 
                         context_menu.draw(list_origin, available_space, cx);
@@ -1217,7 +1397,6 @@ impl EditorElement {
         &mut self,
         bounds: Bounds<Pixels>,
         layout: &mut LayoutState,
-        editor: &mut Editor,
         cx: &mut WindowContext,
     ) {
         let scroll_position = layout.position_map.snapshot.scroll_position();
@@ -1237,7 +1416,7 @@ impl EditorElement {
         }
     }
 
-    fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> Pixels {
+    fn column_pixels(&self, column: usize, cx: &WindowContext) -> Pixels {
         let style = &self.style;
         let font_size = style.text.font_size.to_pixels(cx.rem_size());
         let layout = cx
@@ -1258,7 +1437,7 @@ impl EditorElement {
         layout.width
     }
 
-    fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> Pixels {
+    fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels {
         let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1;
         self.column_pixels(digit_count, cx)
     }
@@ -1413,7 +1592,7 @@ impl EditorElement {
     }
 
     fn layout_lines(
-        &mut self,
+        &self,
         rows: Range<u32>,
         line_number_layouts: &[Option<ShapedLine>],
         snapshot: &EditorSnapshot,
@@ -1469,483 +1648,469 @@ impl EditorElement {
 
     fn compute_layout(
         &mut self,
-        editor: &mut Editor,
-        cx: &mut ViewContext<'_, Editor>,
         mut bounds: Bounds<Pixels>,
+        cx: &mut WindowContext,
     ) -> LayoutState {
-        // let mut size = constraint.max;
-        // if size.x.is_infinite() {
-        //     unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
-        // }
-
-        let snapshot = editor.snapshot(cx);
-        let style = self.style.clone();
+        self.editor.update(cx, |editor, cx| {
+            // let mut size = constraint.max;
+            // if size.x.is_infinite() {
+            //     unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
+            // }
+
+            let snapshot = editor.snapshot(cx);
+            let style = self.style.clone();
+
+            let font_id = cx.text_system().font_id(&style.text.font()).unwrap();
+            let font_size = style.text.font_size.to_pixels(cx.rem_size());
+            let line_height = style.text.line_height_in_pixels(cx.rem_size());
+            let em_width = cx
+                .text_system()
+                .typographic_bounds(font_id, font_size, 'm')
+                .unwrap()
+                .size
+                .width;
+            let em_advance = cx
+                .text_system()
+                .advance(font_id, font_size, 'm')
+                .unwrap()
+                .width;
+
+            let gutter_padding;
+            let gutter_width;
+            let gutter_margin;
+            if snapshot.show_gutter {
+                let descent = cx.text_system().descent(font_id, font_size).unwrap();
+
+                let gutter_padding_factor = 3.5;
+                gutter_padding = (em_width * gutter_padding_factor).round();
+                gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
+                gutter_margin = -descent;
+            } else {
+                gutter_padding = Pixels::ZERO;
+                gutter_width = Pixels::ZERO;
+                gutter_margin = Pixels::ZERO;
+            };
 
-        let font_id = cx.text_system().font_id(&style.text.font()).unwrap();
-        let font_size = style.text.font_size.to_pixels(cx.rem_size());
-        let line_height = style.text.line_height_in_pixels(cx.rem_size());
-        let em_width = cx
-            .text_system()
-            .typographic_bounds(font_id, font_size, 'm')
-            .unwrap()
-            .size
-            .width;
-        let em_advance = cx
-            .text_system()
-            .advance(font_id, font_size, 'm')
-            .unwrap()
-            .width;
-
-        let gutter_padding;
-        let gutter_width;
-        let gutter_margin;
-        if snapshot.show_gutter {
-            let descent = cx.text_system().descent(font_id, font_size).unwrap();
-
-            let gutter_padding_factor = 3.5;
-            gutter_padding = (em_width * gutter_padding_factor).round();
-            gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
-            gutter_margin = -descent;
-        } else {
-            gutter_padding = Pixels::ZERO;
-            gutter_width = Pixels::ZERO;
-            gutter_margin = Pixels::ZERO;
-        };
+            editor.gutter_width = gutter_width;
+            let text_width = bounds.size.width - gutter_width;
+            let overscroll = size(em_width, px(0.));
+            let snapshot = {
+                editor.set_visible_line_count((bounds.size.height / line_height).into(), cx);
+
+                let editor_width = text_width - gutter_margin - overscroll.width - em_width;
+                let wrap_width = match editor.soft_wrap_mode(cx) {
+                    SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
+                    SoftWrap::EditorWidth => editor_width,
+                    SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
+                };
 
-        editor.gutter_width = gutter_width;
-        let text_width = bounds.size.width - gutter_width;
-        let overscroll = size(em_width, px(0.));
-        let snapshot = {
-            editor.set_visible_line_count((bounds.size.height / line_height).into(), cx);
-
-            let editor_width = text_width - gutter_margin - overscroll.width - em_width;
-            let wrap_width = match editor.soft_wrap_mode(cx) {
-                SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
-                SoftWrap::EditorWidth => editor_width,
-                SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
+                if editor.set_wrap_width(Some(wrap_width), cx) {
+                    editor.snapshot(cx)
+                } else {
+                    snapshot
+                }
             };
 
-            if editor.set_wrap_width(Some(wrap_width), cx) {
-                editor.snapshot(cx)
+            let wrap_guides = editor
+                .wrap_guides(cx)
+                .iter()
+                .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
+                .collect::<SmallVec<[_; 2]>>();
+
+            let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height;
+            // todo!("this should happen during layout")
+            let editor_mode = snapshot.mode;
+            if let EditorMode::AutoHeight { max_lines } = editor_mode {
+                todo!()
+                //     size.set_y(
+                //         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 = editor_mode {
+                bounds.size.height = line_height.min(bounds.size.height);
+            }
+            // todo!()
+            // else if size.y.is_infinite() {
+            //     //     size.set_y(scroll_height);
+            // }
+            //
+            let gutter_size = size(gutter_width, bounds.size.height);
+            let text_size = size(text_width, bounds.size.height);
+
+            let autoscroll_horizontally =
+                editor.autoscroll_vertically(bounds.size.height, line_height, cx);
+            let mut snapshot = editor.snapshot(cx);
+
+            let scroll_position = snapshot.scroll_position();
+            // The scroll position is a fractional point, the whole number of which represents
+            // the top of the window in terms of display rows.
+            let start_row = scroll_position.y as u32;
+            let height_in_lines = f32::from(bounds.size.height / line_height);
+            let max_row = snapshot.max_point().row();
+
+            // Add 1 to ensure selections bleed off screen
+            let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row);
+
+            let start_anchor = if start_row == 0 {
+                Anchor::min()
             } else {
                 snapshot
-            }
-        };
+                    .buffer_snapshot
+                    .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
+            };
+            let end_anchor = if end_row > max_row {
+                Anchor::max()
+            } else {
+                snapshot
+                    .buffer_snapshot
+                    .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
+            };
 
-        let wrap_guides = editor
-            .wrap_guides(cx)
-            .iter()
-            .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
-            .collect::<SmallVec<[_; 2]>>();
-
-        let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height;
-        // todo!("this should happen during layout")
-        let editor_mode = snapshot.mode;
-        if let EditorMode::AutoHeight { max_lines } = editor_mode {
-            todo!()
-            //     size.set_y(
-            //         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 = editor_mode {
-            bounds.size.height = line_height.min(bounds.size.height);
-        }
-        // todo!()
-        // else if size.y.is_infinite() {
-        //     //     size.set_y(scroll_height);
-        // }
-        //
-        let gutter_size = size(gutter_width, bounds.size.height);
-        let text_size = size(text_width, bounds.size.height);
-
-        let autoscroll_horizontally =
-            editor.autoscroll_vertically(bounds.size.height, line_height, cx);
-        let mut snapshot = editor.snapshot(cx);
-
-        let scroll_position = snapshot.scroll_position();
-        // The scroll position is a fractional point, the whole number of which represents
-        // the top of the window in terms of display rows.
-        let start_row = scroll_position.y as u32;
-        let height_in_lines = f32::from(bounds.size.height / line_height);
-        let max_row = snapshot.max_point().row();
-
-        // Add 1 to ensure selections bleed off screen
-        let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row);
-
-        let start_anchor = if start_row == 0 {
-            Anchor::min()
-        } else {
-            snapshot
-                .buffer_snapshot
-                .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
-        };
-        let end_anchor = if end_row > max_row {
-            Anchor::max()
-        } else {
-            snapshot
-                .buffer_snapshot
-                .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
-        };
+            let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
+            let mut active_rows = BTreeMap::new();
+            let is_singleton = editor.is_singleton(cx);
 
-        let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
-        let mut active_rows = BTreeMap::new();
-        let is_singleton = editor.is_singleton(cx);
+            let highlighted_rows = editor.highlighted_rows();
+            let highlighted_ranges = editor.background_highlights_in_range(
+                start_anchor..end_anchor,
+                &snapshot.display_snapshot,
+                cx.theme().colors(),
+            );
 
-        let highlighted_rows = editor.highlighted_rows();
-        let highlighted_ranges = editor.background_highlights_in_range(
-            start_anchor..end_anchor,
-            &snapshot.display_snapshot,
-            cx.theme().colors(),
-        );
+            let mut newest_selection_head = None;
+
+            if editor.show_local_selections {
+                let mut local_selections: Vec<Selection<Point>> = editor
+                    .selections
+                    .disjoint_in_range(start_anchor..end_anchor, cx);
+                local_selections.extend(editor.selections.pending(cx));
+                let mut layouts = Vec::new();
+                let newest = editor.selections.newest(cx);
+                for selection in local_selections.drain(..) {
+                    let is_empty = selection.start == selection.end;
+                    let is_newest = selection == newest;
+
+                    let layout = SelectionLayout::new(
+                        selection,
+                        editor.selections.line_mode,
+                        editor.cursor_shape,
+                        &snapshot.display_snapshot,
+                        is_newest,
+                        true,
+                    );
+                    if is_newest {
+                        newest_selection_head = Some(layout.head);
+                    }
 
-        let mut newest_selection_head = None;
-
-        if editor.show_local_selections {
-            let mut local_selections: Vec<Selection<Point>> = editor
-                .selections
-                .disjoint_in_range(start_anchor..end_anchor, cx);
-            local_selections.extend(editor.selections.pending(cx));
-            let mut layouts = Vec::new();
-            let newest = editor.selections.newest(cx);
-            for selection in local_selections.drain(..) {
-                let is_empty = selection.start == selection.end;
-                let is_newest = selection == newest;
-
-                let layout = SelectionLayout::new(
-                    selection,
-                    editor.selections.line_mode,
-                    editor.cursor_shape,
-                    &snapshot.display_snapshot,
-                    is_newest,
-                    true,
-                );
-                if is_newest {
-                    newest_selection_head = Some(layout.head);
+                    for row in cmp::max(layout.active_rows.start, start_row)
+                        ..=cmp::min(layout.active_rows.end, end_row)
+                    {
+                        let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
+                        *contains_non_empty_selection |= !is_empty;
+                    }
+                    layouts.push(layout);
                 }
 
-                for row in cmp::max(layout.active_rows.start, start_row)
-                    ..=cmp::min(layout.active_rows.end, end_row)
-                {
-                    let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
-                    *contains_non_empty_selection |= !is_empty;
-                }
-                layouts.push(layout);
+                selections.push((style.local_player, layouts));
             }
 
-            selections.push((style.local_player, layouts));
-        }
-
-        if let Some(collaboration_hub) = &editor.collaboration_hub {
-            // When following someone, render the local selections in their color.
-            if let Some(leader_id) = editor.leader_peer_id {
-                if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
-                    if let Some(participant_index) = collaboration_hub
-                        .user_participant_indices(cx)
-                        .get(&collaborator.user_id)
-                    {
-                        if let Some((local_selection_style, _)) = selections.first_mut() {
-                            *local_selection_style = cx
-                                .theme()
-                                .players()
-                                .color_for_participant(participant_index.0);
+            if let Some(collaboration_hub) = &editor.collaboration_hub {
+                // When following someone, render the local selections in their color.
+                if let Some(leader_id) = editor.leader_peer_id {
+                    if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
+                        if let Some(participant_index) = collaboration_hub
+                            .user_participant_indices(cx)
+                            .get(&collaborator.user_id)
+                        {
+                            if let Some((local_selection_style, _)) = selections.first_mut() {
+                                *local_selection_style = cx
+                                    .theme()
+                                    .players()
+                                    .color_for_participant(participant_index.0);
+                            }
                         }
                     }
                 }
-            }
 
-            let mut remote_selections = HashMap::default();
-            for selection in snapshot.remote_selections_in_range(
-                &(start_anchor..end_anchor),
-                collaboration_hub.as_ref(),
-                cx,
-            ) {
-                let selection_style = if let Some(participant_index) = selection.participant_index {
-                    cx.theme()
-                        .players()
-                        .color_for_participant(participant_index.0)
-                } else {
-                    cx.theme().players().absent()
-                };
-
-                // Don't re-render the leader's selections, since the local selections
-                // match theirs.
-                if Some(selection.peer_id) == editor.leader_peer_id {
-                    continue;
-                }
+                let mut remote_selections = HashMap::default();
+                for selection in snapshot.remote_selections_in_range(
+                    &(start_anchor..end_anchor),
+                    collaboration_hub.as_ref(),
+                    cx,
+                ) {
+                    let selection_style = if let Some(participant_index) = selection.participant_index {
+                        cx.theme()
+                            .players()
+                            .color_for_participant(participant_index.0)
+                    } else {
+                        cx.theme().players().absent()
+                    };
 
-                remote_selections
-                    .entry(selection.replica_id)
-                    .or_insert((selection_style, Vec::new()))
-                    .1
-                    .push(SelectionLayout::new(
-                        selection.selection,
-                        selection.line_mode,
-                        selection.cursor_shape,
-                        &snapshot.display_snapshot,
-                        false,
-                        false,
-                    ));
-            }
+                    // Don't re-render the leader's selections, since the local selections
+                    // match theirs.
+                    if Some(selection.peer_id) == editor.leader_peer_id {
+                        continue;
+                    }
 
-            selections.extend(remote_selections.into_values());
-        }
+                    remote_selections
+                        .entry(selection.replica_id)
+                        .or_insert((selection_style, Vec::new()))
+                        .1
+                        .push(SelectionLayout::new(
+                            selection.selection,
+                            selection.line_mode,
+                            selection.cursor_shape,
+                            &snapshot.display_snapshot,
+                            false,
+                            false,
+                        ));
+                }
 
-        let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
-        let show_scrollbars = match scrollbar_settings.show {
-            ShowScrollbar::Auto => {
-                // Git
-                (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
-                ||
-                // Selections
-                (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
-                // Scrollmanager
-                || editor.scroll_manager.scrollbars_visible()
+                selections.extend(remote_selections.into_values());
             }
-            ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
-            ShowScrollbar::Always => true,
-            ShowScrollbar::Never => false,
-        };
 
-        let head_for_relative = newest_selection_head.unwrap_or_else(|| {
-            let newest = editor.selections.newest::<Point>(cx);
-            SelectionLayout::new(
-                newest,
-                editor.selections.line_mode,
-                editor.cursor_shape,
-                &snapshot.display_snapshot,
-                true,
-                true,
-            )
-            .head
-        });
-
-        let (line_numbers, fold_statuses) = self.shape_line_numbers(
-            start_row..end_row,
-            &active_rows,
-            head_for_relative,
-            is_singleton,
-            &snapshot,
-            cx,
-        );
-
-        let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
-
-        let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
-
-        let mut max_visible_line_width = Pixels::ZERO;
-        let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
-        for line_with_invisibles in &line_layouts {
-            if line_with_invisibles.line.width > max_visible_line_width {
-                max_visible_line_width = line_with_invisibles.line.width;
-            }
-        }
+            let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
+            let show_scrollbars = match scrollbar_settings.show {
+                ShowScrollbar::Auto => {
+                    // Git
+                    (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
+                    ||
+                    // Selections
+                    (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
+                    // Scrollmanager
+                    || editor.scroll_manager.scrollbars_visible()
+                }
+                ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
+                ShowScrollbar::Always => true,
+                ShowScrollbar::Never => false,
+            };
 
-        let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx)
-            .unwrap()
-            .width;
-        let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width;
+            let head_for_relative = newest_selection_head.unwrap_or_else(|| {
+                let newest = editor.selections.newest::<Point>(cx);
+                SelectionLayout::new(
+                    newest,
+                    editor.selections.line_mode,
+                    editor.cursor_shape,
+                    &snapshot.display_snapshot,
+                    true,
+                    true,
+                )
+                .head
+            });
 
-        let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| {
-            self.layout_blocks(
+            let (line_numbers, fold_statuses) = self.shape_line_numbers(
                 start_row..end_row,
+                &active_rows,
+                head_for_relative,
+                is_singleton,
                 &snapshot,
-                bounds.size.width,
-                scroll_width,
-                gutter_padding,
-                gutter_width,
-                em_width,
-                gutter_width + gutter_margin,
-                line_height,
-                &style,
-                &line_layouts,
-                editor,
                 cx,
-            )
-        });
+            );
 
-        let scroll_max = point(
-            f32::from((scroll_width - text_size.width) / em_width).max(0.0),
-            max_row as f32,
-        );
+            let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
 
-        let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
+            let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
 
-        let autoscrolled = if autoscroll_horizontally {
-            editor.autoscroll_horizontally(
-                start_row,
-                text_size.width,
-                scroll_width,
-                em_width,
-                &line_layouts,
-                cx,
-            )
-        } else {
-            false
-        };
+            let mut max_visible_line_width = Pixels::ZERO;
+            let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
+            for line_with_invisibles in &line_layouts {
+                if line_with_invisibles.line.width > max_visible_line_width {
+                    max_visible_line_width = line_with_invisibles.line.width;
+                }
+            }
 
-        if clamped || autoscrolled {
-            snapshot = editor.snapshot(cx);
-        }
+            let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx)
+                .unwrap()
+                .width;
+            let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width;
 
-        let mut context_menu = None;
-        let mut code_actions_indicator = None;
-        if let Some(newest_selection_head) = newest_selection_head {
-            if (start_row..end_row).contains(&newest_selection_head.row()) {
-                if editor.context_menu_visible() {
-                    context_menu =
-                        editor.render_context_menu(newest_selection_head, &self.style, cx);
-                }
+            let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| {
+                self.layout_blocks(
+                    start_row..end_row,
+                    &snapshot,
+                    bounds.size.width,
+                    scroll_width,
+                    gutter_padding,
+                    gutter_width,
+                    em_width,
+                    gutter_width + gutter_margin,
+                    line_height,
+                    &style,
+                    &line_layouts,
+                    editor,
+                    cx,
+                )
+            });
 
-                let active = matches!(
-                    editor.context_menu.read().as_ref(),
-                    Some(crate::ContextMenu::CodeActions(_))
-                );
+            let scroll_max = point(
+                f32::from((scroll_width - text_size.width) / em_width).max(0.0),
+                max_row as f32,
+            );
 
-                code_actions_indicator = editor
-                    .render_code_actions_indicator(&style, active, cx)
-                    .map(|element| CodeActionsIndicator {
-                        row: newest_selection_head.row(),
-                        button: element,
-                    });
+            let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
+
+            let autoscrolled = if autoscroll_horizontally {
+                editor.autoscroll_horizontally(
+                    start_row,
+                    text_size.width,
+                    scroll_width,
+                    em_width,
+                    &line_layouts,
+                    cx,
+                )
+            } else {
+                false
+            };
+
+            if clamped || autoscrolled {
+                snapshot = editor.snapshot(cx);
             }
-        }
 
-        let visible_rows = start_row..start_row + line_layouts.len() as u32;
-        // todo!("hover")
-        // let mut hover = editor.hover_state.render(
-        //     &snapshot,
-        //     &style,
-        //     visible_rows,
-        //     editor.workspace.as_ref().map(|(w, _)| w.clone()),
-        //     cx,
-        // );
-        // let mode = editor.mode;
-
-        let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
-            editor.render_fold_indicators(
-                fold_statuses,
-                &style,
-                editor.gutter_hovered,
-                line_height,
-                gutter_margin,
-                cx,
-            )
-        });
+            let mut context_menu = None;
+            let mut code_actions_indicator = None;
+            if let Some(newest_selection_head) = newest_selection_head {
+                if (start_row..end_row).contains(&newest_selection_head.row()) {
+                    if editor.context_menu_visible() {
+                        context_menu =
+                            editor.render_context_menu(newest_selection_head, &self.style, cx);
+                    }
 
-        // todo!("context_menu")
-        // if let Some((_, context_menu)) = context_menu.as_mut() {
-        //     context_menu.layout(
-        //         SizeConstraint {
-        //             min: gpui::Point::<Pixels>::zero(),
-        //             max: point(
-        //                 cx.window_size().x * 0.7,
-        //                 (12. * line_height).min((size.y - line_height) / 2.),
-        //             ),
-        //         },
-        //         editor,
-        //         cx,
-        //     );
-        // }
+                    let active = matches!(
+                        editor.context_menu.read().as_ref(),
+                        Some(crate::ContextMenu::CodeActions(_))
+                    );
 
-        // todo!("hover popovers")
-        // if let Some((_, hover_popovers)) = hover.as_mut() {
-        //     for hover_popover in hover_popovers.iter_mut() {
-        //         hover_popover.layout(
-        //             SizeConstraint {
-        //                 min: gpui::Point::<Pixels>::zero(),
-        //                 max: point(
-        //                     (120. * em_width) // Default size
-        //                         .min(size.x / 2.) // Shrink to half of the editor width
-        //                         .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
-        //                     (16. * line_height) // Default size
-        //                         .min(size.y / 2.) // Shrink to half of the editor height
-        //                         .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
-        //                 ),
-        //             },
-        //             editor,
-        //             cx,
-        //         );
-        //     }
-        // }
+                    code_actions_indicator = editor
+                        .render_code_actions_indicator(&style, active, cx)
+                        .map(|element| CodeActionsIndicator {
+                            row: newest_selection_head.row(),
+                            button: element,
+                        });
+                }
+            }
 
-        let invisible_symbol_font_size = font_size / 2.;
-        let tab_invisible = cx
-            .text_system()
-            .shape_line(
-                "→".into(),
-                invisible_symbol_font_size,
-                &[TextRun {
-                    len: "→".len(),
-                    font: self.style.text.font(),
-                    color: cx.theme().colors().editor_invisible,
-                    background_color: None,
-                    underline: None,
-                }],
-            )
-            .unwrap();
-        let space_invisible = cx
-            .text_system()
-            .shape_line(
-                "•".into(),
-                invisible_symbol_font_size,
-                &[TextRun {
-                    len: "•".len(),
-                    font: self.style.text.font(),
-                    color: cx.theme().colors().editor_invisible,
-                    background_color: None,
-                    underline: None,
-                }],
-            )
-            .unwrap();
+            let visible_rows = start_row..start_row + line_layouts.len() as u32;
+            // todo!("hover")
+            // let mut hover = editor.hover_state.render(
+            //     &snapshot,
+            //     &style,
+            //     visible_rows,
+            //     editor.workspace.as_ref().map(|(w, _)| w.clone()),
+            //     cx,
+            // );
+            // let mode = editor.mode;
+
+            let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
+                editor.render_fold_indicators(
+                    fold_statuses,
+                    &style,
+                    editor.gutter_hovered,
+                    line_height,
+                    gutter_margin,
+                    cx,
+                )
+            });
 
-        LayoutState {
-            mode: editor_mode,
-            position_map: Arc::new(PositionMap {
-                size: bounds.size,
-                scroll_position: point(
-                    scroll_position.x * em_width,
-                    scroll_position.y * line_height,
-                ),
-                scroll_max,
-                line_layouts,
-                line_height,
-                em_width,
-                em_advance,
-                snapshot,
-            }),
-            visible_anchor_range: start_anchor..end_anchor,
-            visible_display_row_range: start_row..end_row,
-            wrap_guides,
-            gutter_size,
-            gutter_padding,
-            text_size,
-            scrollbar_row_range,
-            show_scrollbars,
-            is_singleton,
-            max_row,
-            gutter_margin,
-            active_rows,
-            highlighted_rows,
-            highlighted_ranges,
-            line_numbers,
-            display_hunks,
-            blocks,
-            selections,
-            context_menu,
-            code_actions_indicator,
-            fold_indicators,
-            tab_invisible,
-            space_invisible,
-            // hover_popovers: hover,
-        }
+            // todo!("hover popovers")
+            // if let Some((_, hover_popovers)) = hover.as_mut() {
+            //     for hover_popover in hover_popovers.iter_mut() {
+            //         hover_popover.layout(
+            //             SizeConstraint {
+            //                 min: gpui::Point::<Pixels>::zero(),
+            //                 max: point(
+            //                     (120. * em_width) // Default size
+            //                         .min(size.x / 2.) // Shrink to half of the editor width
+            //                         .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
+            //                     (16. * line_height) // Default size
+            //                         .min(size.y / 2.) // Shrink to half of the editor height
+            //                         .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
+            //                 ),
+            //             },
+            //             editor,
+            //             cx,
+            //         );
+            //     }
+            // }
+
+            let invisible_symbol_font_size = font_size / 2.;
+            let tab_invisible = cx
+                .text_system()
+                .shape_line(
+                    "→".into(),
+                    invisible_symbol_font_size,
+                    &[TextRun {
+                        len: "→".len(),
+                        font: self.style.text.font(),
+                        color: cx.theme().colors().editor_invisible,
+                        background_color: None,
+                        underline: None,
+                    }],
+                )
+                .unwrap();
+            let space_invisible = cx
+                .text_system()
+                .shape_line(
+                    "•".into(),
+                    invisible_symbol_font_size,
+                    &[TextRun {
+                        len: "•".len(),
+                        font: self.style.text.font(),
+                        color: cx.theme().colors().editor_invisible,
+                        background_color: None,
+                        underline: None,
+                    }],
+                )
+                .unwrap();
+
+            LayoutState {
+                mode: editor_mode,
+                position_map: Arc::new(PositionMap {
+                    size: bounds.size,
+                    scroll_position: point(
+                        scroll_position.x * em_width,
+                        scroll_position.y * line_height,
+                    ),
+                    scroll_max,
+                    line_layouts,
+                    line_height,
+                    em_width,
+                    em_advance,
+                    snapshot,
+                }),
+                visible_anchor_range: start_anchor..end_anchor,
+                visible_display_row_range: start_row..end_row,
+                wrap_guides,
+                gutter_size,
+                gutter_padding,
+                text_size,
+                scrollbar_row_range,
+                show_scrollbars,
+                is_singleton,
+                max_row,
+                gutter_margin,
+                active_rows,
+                highlighted_rows,
+                highlighted_ranges,
+                line_numbers,
+                display_hunks,
+                blocks,
+                selections,
+                context_menu,
+                code_actions_indicator,
+                fold_indicators,
+                tab_invisible,
+                space_invisible,
+                // hover_popovers: hover,
+            }
+        })
     }
 
     #[allow(clippy::too_many_arguments)]
     fn layout_blocks(
-        &mut self,
+        &self,
         rows: Range<u32>,
         snapshot: &EditorSnapshot,
         editor_width: Pixels,

crates/gpui2/src/elements/text.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
     Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun,
-    WindowContext, WrappedLine,
+    WhiteSpace, WindowContext, WrappedLine,
 };
 use anyhow::anyhow;
 use parking_lot::{Mutex, MutexGuard};
@@ -159,10 +159,14 @@ impl TextState {
             let element_state = self.clone();
 
             move |known_dimensions, available_space| {
-                let wrap_width = known_dimensions.width.or(match available_space.width {
-                    crate::AvailableSpace::Definite(x) => Some(x),
-                    _ => None,
-                });
+                let wrap_width = if text_style.white_space == WhiteSpace::Normal {
+                    known_dimensions.width.or(match available_space.width {
+                        crate::AvailableSpace::Definite(x) => Some(x),
+                        _ => None,
+                    })
+                } else {
+                    None
+                };
 
                 if let Some(text_state) = element_state.0.lock().as_ref() {
                     if text_state.size.is_some()
@@ -174,10 +178,7 @@ impl TextState {
 
                 let Some(lines) = text_system
                     .shape_text(
-                        &text,
-                        font_size,
-                        &runs[..],
-                        wrap_width, // Wrap if we know the width.
+                        &text, font_size, &runs, wrap_width, // Wrap if we know the width.
                     )
                     .log_err()
                 else {
@@ -194,7 +195,7 @@ impl TextState {
                 for line in &lines {
                     let line_size = line.size(line_height);
                     size.height += line_size.height;
-                    size.width = size.width.max(line_size.width);
+                    size.width = size.width.max(line_size.width).ceil();
                 }
 
                 element_state.lock().replace(TextStateInner {

crates/gpui2/src/elements/uniform_list.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
-    point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement,
-    InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size,
-    StyleRefinement, Styled, View, ViewContext, WindowContext,
+    point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
+    ElementId, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels, Point,
+    Render, RenderOnce, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
 };
 use smallvec::SmallVec;
 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -22,8 +22,8 @@ where
     V: Render,
 {
     let id = id.into();
-    let mut style = StyleRefinement::default();
-    style.overflow.y = Some(Overflow::Hidden);
+    let mut base_style = StyleRefinement::default();
+    base_style.overflow.y = Some(Overflow::Scroll);
 
     let render_range = move |range, cx: &mut WindowContext| {
         view.update(cx, |this, cx| {
@@ -36,12 +36,12 @@ where
 
     UniformList {
         id: id.clone(),
-        style,
         item_count,
         item_to_measure_index: 0,
         render_items: Box::new(render_range),
         interactivity: Interactivity {
             element_id: Some(id.into()),
+            base_style,
             ..Default::default()
         },
         scroll_handle: None,
@@ -50,7 +50,6 @@ where
 
 pub struct UniformList {
     id: ElementId,
-    style: StyleRefinement,
     item_count: usize,
     item_to_measure_index: usize,
     render_items:
@@ -91,7 +90,7 @@ impl UniformListScrollHandle {
 
 impl Styled for UniformList {
     fn style(&mut self) -> &mut StyleRefinement {
-        &mut self.style
+        &mut self.interactivity.base_style
     }
 }
 
@@ -211,31 +210,31 @@ impl Element for UniformList {
                                 scroll_offset: shared_scroll_offset,
                             });
                         }
-                        let visible_item_count = if item_height > px(0.) {
-                            (padded_bounds.size.height / item_height).ceil() as usize + 1
-                        } else {
-                            0
-                        };
 
                         let first_visible_element_ix =
                             (-scroll_offset.y / item_height).floor() as usize;
+                        let last_visible_element_ix =
+                            ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
+                                as usize;
                         let visible_range = first_visible_element_ix
-                            ..cmp::min(
-                                first_visible_element_ix + visible_item_count,
-                                self.item_count,
-                            );
+                            ..cmp::min(last_visible_element_ix, self.item_count);
 
                         let items = (self.render_items)(visible_range.clone(), cx);
                         cx.with_z_index(1, |cx| {
-                            for (item, ix) in items.into_iter().zip(visible_range) {
-                                let item_origin = padded_bounds.origin
-                                    + point(px(0.), item_height * ix + scroll_offset.y);
-                                let available_space = size(
-                                    AvailableSpace::Definite(padded_bounds.size.width),
-                                    AvailableSpace::Definite(item_height),
-                                );
-                                item.draw(item_origin, available_space, cx);
-                            }
+                            let content_mask = ContentMask {
+                                bounds: padded_bounds,
+                            };
+                            cx.with_content_mask(Some(content_mask), |cx| {
+                                for (item, ix) in items.into_iter().zip(visible_range) {
+                                    let item_origin = padded_bounds.origin
+                                        + point(px(0.), item_height * ix + scroll_offset.y);
+                                    let available_space = size(
+                                        AvailableSpace::Definite(padded_bounds.size.width),
+                                        AvailableSpace::Definite(item_height),
+                                    );
+                                    item.draw(item_origin, available_space, cx);
+                                }
+                            });
                         });
                     }
                 })

crates/gpui2/src/style.rs 🔗

@@ -128,6 +128,13 @@ pub struct BoxShadow {
     pub spread_radius: Pixels,
 }
 
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum WhiteSpace {
+    #[default]
+    Normal,
+    Nowrap,
+}
+
 #[derive(Refineable, Clone, Debug)]
 #[refineable(Debug)]
 pub struct TextStyle {
@@ -139,6 +146,7 @@ pub struct TextStyle {
     pub font_weight: FontWeight,
     pub font_style: FontStyle,
     pub underline: Option<UnderlineStyle>,
+    pub white_space: WhiteSpace,
 }
 
 impl Default for TextStyle {
@@ -152,6 +160,7 @@ impl Default for TextStyle {
             font_weight: FontWeight::default(),
             font_style: FontStyle::default(),
             underline: None,
+            white_space: WhiteSpace::Normal,
         }
     }
 }

crates/gpui2/src/styled.rs 🔗

@@ -1,7 +1,7 @@
 use crate::{
     self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
     DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
-    SharedString, StyleRefinement, Visibility,
+    SharedString, StyleRefinement, Visibility, WhiteSpace,
 };
 use crate::{BoxShadow, TextStyleRefinement};
 use smallvec::{smallvec, SmallVec};
@@ -101,6 +101,24 @@ pub trait Styled: Sized {
         self
     }
 
+    /// Sets the whitespace of the element to `normal`.
+    /// [Docs](https://tailwindcss.com/docs/whitespace#normal)
+    fn whitespace_normal(mut self) -> Self {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .white_space = Some(WhiteSpace::Normal);
+        self
+    }
+
+    /// Sets the whitespace of the element to `nowrap`.
+    /// [Docs](https://tailwindcss.com/docs/whitespace#nowrap)
+    fn whitespace_nowrap(mut self) -> Self {
+        self.text_style()
+            .get_or_insert_with(Default::default)
+            .white_space = Some(WhiteSpace::Nowrap);
+        self
+    }
+
     /// Sets the flex direction of the element to `column`.
     /// [Docs](https://tailwindcss.com/docs/flex-direction#column)
     fn flex_col(mut self) -> Self {

crates/language2/src/buffer.rs 🔗

@@ -7,6 +7,7 @@ pub use crate::{
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
     language_settings::{language_settings, LanguageSettings},
+    markdown::parse_markdown,
     outline::OutlineItem,
     syntax_map::{
         SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches,
@@ -155,12 +156,52 @@ pub struct Diagnostic {
     pub is_unnecessary: bool,
 }
 
+pub async fn prepare_completion_documentation(
+    documentation: &lsp::Documentation,
+    language_registry: &Arc<LanguageRegistry>,
+    language: Option<Arc<Language>>,
+) -> Documentation {
+    match documentation {
+        lsp::Documentation::String(text) => {
+            if text.lines().count() <= 1 {
+                Documentation::SingleLine(text.clone())
+            } else {
+                Documentation::MultiLinePlainText(text.clone())
+            }
+        }
+
+        lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind {
+            lsp::MarkupKind::PlainText => {
+                if value.lines().count() <= 1 {
+                    Documentation::SingleLine(value.clone())
+                } else {
+                    Documentation::MultiLinePlainText(value.clone())
+                }
+            }
+
+            lsp::MarkupKind::Markdown => {
+                let parsed = parse_markdown(value, language_registry, language).await;
+                Documentation::MultiLineMarkdown(parsed)
+            }
+        },
+    }
+}
+
+#[derive(Clone, Debug)]
+pub enum Documentation {
+    Undocumented,
+    SingleLine(String),
+    MultiLinePlainText(String),
+    MultiLineMarkdown(ParsedMarkdown),
+}
+
 #[derive(Clone, Debug)]
 pub struct Completion {
     pub old_range: Range<Anchor>,
     pub new_text: String,
     pub label: CodeLabel,
     pub server_id: LanguageServerId,
+    pub documentation: Option<Documentation>,
     pub lsp_completion: lsp::CompletionItem,
 }
 

crates/language2/src/proto.rs 🔗

@@ -482,6 +482,7 @@ pub async fn deserialize_completion(
                 lsp_completion.filter_text.as_deref(),
             )
         }),
+        documentation: None,
         server_id: LanguageServerId(completion.server_id as usize),
         lsp_completion,
     })

crates/project2/src/lsp_command.rs 🔗

@@ -10,7 +10,7 @@ use futures::future;
 use gpui::{AppContext, AsyncAppContext, Model};
 use language::{
     language_settings::{language_settings, InlayHintKind},
-    point_from_lsp, point_to_lsp,
+    point_from_lsp, point_to_lsp, prepare_completion_documentation,
     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,
@@ -1339,7 +1339,7 @@ impl LspCommand for GetCompletions {
     async fn response_from_lsp(
         self,
         completions: Option<lsp::CompletionResponse>,
-        _: Model<Project>,
+        project: Model<Project>,
         buffer: Model<Buffer>,
         server_id: LanguageServerId,
         mut cx: AsyncAppContext,
@@ -1359,7 +1359,8 @@ impl LspCommand for GetCompletions {
             Default::default()
         };
 
-        let completions = buffer.update(&mut cx, |buffer, _| {
+        let completions = buffer.update(&mut cx, |buffer, cx| {
+            let language_registry = project.read(cx).languages().clone();
             let language = buffer.language().cloned();
             let snapshot = buffer.snapshot();
             let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
@@ -1443,14 +1444,29 @@ impl LspCommand for GetCompletions {
                         }
                     };
 
+                    let language_registry = language_registry.clone();
                     let language = language.clone();
                     LineEnding::normalize(&mut new_text);
                     Some(async move {
                         let mut label = None;
-                        if let Some(language) = language {
+                        if let Some(language) = language.as_ref() {
                             language.process_completion(&mut lsp_completion).await;
                             label = language.label_for_completion(&lsp_completion).await;
                         }
+
+                        let documentation = if let Some(lsp_docs) = &lsp_completion.documentation {
+                            Some(
+                                prepare_completion_documentation(
+                                    lsp_docs,
+                                    &language_registry,
+                                    language.clone(),
+                                )
+                                .await,
+                            )
+                        } else {
+                            None
+                        };
+
                         Completion {
                             old_range,
                             new_text,
@@ -1460,6 +1476,7 @@ impl LspCommand for GetCompletions {
                                     lsp_completion.filter_text.as_deref(),
                                 )
                             }),
+                            documentation,
                             server_id,
                             lsp_completion,
                         }

crates/project_panel2/src/project_panel.rs 🔗

@@ -371,7 +371,7 @@ impl ProjectPanel {
         _entry_id: ProjectEntryId,
         _cx: &mut ViewContext<Self>,
     ) {
-        todo!()
+        // todo!()
         //     let project = self.project.read(cx);
 
         //     let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -31,7 +31,7 @@ use workspace::{
     notifications::NotifyResultExt,
     register_deserializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem},
-    ui::{ContextMenu, Icon, IconElement, Label, ListItem},
+    ui::{ContextMenu, Icon, IconElement, Label},
     CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
 };
 
@@ -299,11 +299,8 @@ impl TerminalView {
         cx: &mut ViewContext<Self>,
     ) {
         self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
-            menu.action(ListItem::new("clear", Label::new("Clear")), Box::new(Clear))
-                .action(
-                    ListItem::new("close", Label::new("Close")),
-                    Box::new(CloseActiveItem { save_intent: None }),
-                )
+            menu.action("Clear", Box::new(Clear))
+                .action("Close", Box::new(CloseActiveItem { save_intent: None }))
         }));
         dbg!(&position);
         // todo!()

crates/ui2/src/components/context_menu.rs 🔗

@@ -1,7 +1,7 @@
 use std::cell::RefCell;
 use std::rc::Rc;
 
-use crate::{prelude::*, v_stack, List};
+use crate::{prelude::*, v_stack, Label, List};
 use crate::{ListItem, ListSeparator, ListSubHeader};
 use gpui::{
     overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DispatchPhase,
@@ -10,9 +10,9 @@ use gpui::{
 };
 
 pub enum ContextMenuItem {
-    Separator(ListSeparator),
-    Header(ListSubHeader),
-    Entry(ListItem, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
+    Separator,
+    Header(SharedString),
+    Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
 }
 
 pub struct ContextMenu {
@@ -46,29 +46,30 @@ impl ContextMenu {
     }
 
     pub fn header(mut self, title: impl Into<SharedString>) -> Self {
-        self.items
-            .push(ContextMenuItem::Header(ListSubHeader::new(title)));
+        self.items.push(ContextMenuItem::Header(title.into()));
         self
     }
 
     pub fn separator(mut self) -> Self {
-        self.items.push(ContextMenuItem::Separator(ListSeparator));
+        self.items.push(ContextMenuItem::Separator);
         self
     }
 
     pub fn entry(
         mut self,
-        view: ListItem,
+        label: impl Into<SharedString>,
         on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
     ) -> Self {
         self.items
-            .push(ContextMenuItem::Entry(view, Rc::new(on_click)));
+            .push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
         self
     }
 
-    pub fn action(self, view: ListItem, action: Box<dyn Action>) -> Self {
+    pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
         // todo: add the keybindings to the list entry
-        self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
+        self.entry(label.into(), move |_, cx| {
+            cx.dispatch_action(action.boxed_clone())
+        })
     }
 
     pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -104,16 +105,16 @@ impl Render for ContextMenu {
                 // .border_color(cx.theme().colors().border)
                 .child(
                     List::new().children(self.items.iter().map(|item| match item {
-                        ContextMenuItem::Separator(separator) => {
-                            separator.clone().render_into_any()
+                        ContextMenuItem::Separator => ListSeparator::new().render_into_any(),
+                        ContextMenuItem::Header(header) => {
+                            ListSubHeader::new(header.clone()).render_into_any()
                         }
-                        ContextMenuItem::Header(header) => header.clone().render_into_any(),
                         ContextMenuItem::Entry(entry, callback) => {
                             let callback = callback.clone();
                             let dismiss = cx.listener(|_, _, cx| cx.emit(Manager::Dismiss));
 
-                            entry
-                                .clone()
+                            ListItem::new(entry.clone())
+                                .child(Label::new(entry.clone()))
                                 .on_click(move |event, cx| {
                                     callback(event, cx);
                                     dismiss(event, cx)

crates/ui2/src/components/list.rs 🔗

@@ -245,45 +245,28 @@ pub struct ListItem {
     // TODO: Reintroduce this
     // disclosure_control_style: DisclosureControlVisibility,
     indent_level: u32,
-    label: Label,
     left_slot: Option<GraphicSlot>,
     overflow: OverflowStyle,
     size: ListEntrySize,
     toggle: Toggle,
     variant: ListItemVariant,
     on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
-}
-
-impl Clone for ListItem {
-    fn clone(&self) -> Self {
-        Self {
-            id: self.id.clone(),
-            disabled: self.disabled,
-            indent_level: self.indent_level,
-            label: self.label.clone(),
-            left_slot: self.left_slot.clone(),
-            overflow: self.overflow,
-            size: self.size,
-            toggle: self.toggle,
-            variant: self.variant,
-            on_click: self.on_click.clone(),
-        }
-    }
+    children: SmallVec<[AnyElement; 2]>,
 }
 
 impl ListItem {
-    pub fn new(id: impl Into<ElementId>, label: Label) -> Self {
+    pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
             id: id.into(),
             disabled: false,
             indent_level: 0,
-            label,
             left_slot: None,
             overflow: OverflowStyle::Hidden,
             size: ListEntrySize::default(),
             toggle: Toggle::NotToggleable,
             variant: ListItemVariant::default(),
             on_click: Default::default(),
+            children: SmallVec::new(),
         }
     }
 
@@ -394,11 +377,17 @@ impl Component for ListItem {
                     .relative()
                     .child(disclosure_control(self.toggle))
                     .children(left_content)
-                    .child(self.label),
+                    .children(self.children),
             )
     }
 }
 
+impl ParentElement for ListItem {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+        &mut self.children
+    }
+}
+
 #[derive(RenderOnce, Clone)]
 pub struct ListSeparator;
 

crates/ui2/src/components/stories/context_menu.rs 🔗

@@ -2,7 +2,7 @@ use gpui::{actions, Action, AnchorCorner, Div, Render, View};
 use story::Story;
 
 use crate::prelude::*;
-use crate::{menu_handle, ContextMenu, Label, ListItem};
+use crate::{menu_handle, ContextMenu, Label};
 
 actions!(PrintCurrentDate, PrintBestFood);
 
@@ -10,17 +10,13 @@ fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<C
     ContextMenu::build(cx, |menu, _| {
         menu.header(header)
             .separator()
-            .entry(
-                ListItem::new("Print current time", Label::new("Print current time")),
-                |v, cx| {
-                    println!("dispatching PrintCurrentTime action");
-                    cx.dispatch_action(PrintCurrentDate.boxed_clone())
-                },
-            )
-            .entry(
-                ListItem::new("Print best food", Label::new("Print best food")),
-                |v, cx| cx.dispatch_action(PrintBestFood.boxed_clone()),
-            )
+            .entry("Print current time", |v, cx| {
+                println!("dispatching PrintCurrentTime action");
+                cx.dispatch_action(PrintCurrentDate.boxed_clone())
+            })
+            .entry("Print best foot", |v, cx| {
+                cx.dispatch_action(PrintBestFood.boxed_clone())
+            })
     })
 }
 

crates/workspace2/Cargo.toml 🔗

@@ -20,7 +20,6 @@ test-support = [
 
 [dependencies]
 db2 = { path = "../db2" }
-call2 = { path = "../call2" }
 client2 = { path = "../client2" }
 collections = { path = "../collections" }
 # context_menu = { path = "../context_menu" }
@@ -37,6 +36,7 @@ theme2 = { path = "../theme2" }
 util = { path = "../util" }
 ui = { package = "ui2", path = "../ui2" }
 
+async-trait.workspace = true
 async-recursion = "1.0.0"
 itertools = "0.10"
 bincode = "1.2.1"

crates/workspace2/src/dock.rs 🔗

@@ -8,9 +8,7 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use theme2::ActiveTheme;
-use ui::{
-    h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, ListItem, Tooltip,
-};
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -719,15 +717,9 @@ impl Render for PanelButtons {
                                         && panel.position_is_valid(position, cx)
                                     {
                                         let panel = panel.clone();
-                                        menu = menu.entry(
-                                            ListItem::new(
-                                                position.to_label(),
-                                                Label::new(format!("Dock {}", position.to_label())),
-                                            ),
-                                            move |_, cx| {
-                                                panel.set_position(position, cx);
-                                            },
-                                        )
+                                        menu = menu.entry(position.to_label(), move |_, cx| {
+                                            panel.set_position(position, cx);
+                                        })
                                     }
                                 }
                                 menu

crates/workspace2/src/pane_group.rs 🔗

@@ -1,6 +1,5 @@
 use crate::{AppState, FollowerState, Pane, Workspace};
 use anyhow::{anyhow, bail, Result};
-use call2::ActiveCall;
 use collections::HashMap;
 use db2::sqlez::{
     bindable::{Bind, Column, StaticColumnCount},
@@ -127,7 +126,6 @@ impl PaneGroup {
         &self,
         project: &Model<Project>,
         follower_states: &HashMap<View<Pane>, FollowerState>,
-        active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
         app_state: &Arc<AppState>,
@@ -137,7 +135,6 @@ impl PaneGroup {
             project,
             0,
             follower_states,
-            active_call,
             active_pane,
             zoomed,
             app_state,
@@ -199,7 +196,6 @@ impl Member {
         project: &Model<Project>,
         basis: usize,
         follower_states: &HashMap<View<Pane>, FollowerState>,
-        active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
         app_state: &Arc<AppState>,
@@ -235,7 +231,6 @@ impl Member {
                     project,
                     basis + 1,
                     follower_states,
-                    active_call,
                     active_pane,
                     zoomed,
                     app_state,
@@ -558,7 +553,6 @@ impl PaneAxis {
         project: &Model<Project>,
         basis: usize,
         follower_states: &HashMap<View<Pane>, FollowerState>,
-        active_call: Option<&Model<ActiveCall>>,
         active_pane: &View<Pane>,
         zoomed: Option<&AnyWeakView>,
         app_state: &Arc<AppState>,
@@ -580,7 +574,6 @@ impl PaneAxis {
                             project,
                             basis,
                             follower_states,
-                            active_call,
                             active_pane,
                             zoomed,
                             app_state,

crates/workspace2/src/workspace2.rs 🔗

@@ -16,7 +16,7 @@ mod toolbar;
 mod workspace_settings;
 
 use anyhow::{anyhow, Context as _, Result};
-use call2::ActiveCall;
+use async_trait::async_trait;
 use client2::{
     proto::{self, PeerId},
     Client, TypedEnvelope, UserStore,
@@ -33,8 +33,8 @@ use gpui::{
     AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
     FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext,
     ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task,
-    View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
-    WindowOptions,
+    View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext,
+    WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -210,7 +210,6 @@ pub fn init_settings(cx: &mut AppContext) {
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     init_settings(cx);
     notifications::init(cx);
-
     //     cx.add_global_action({
     //         let app_state = Arc::downgrade(&app_state);
     //         move |_: &Open, cx: &mut AppContext| {
@@ -304,6 +303,7 @@ pub struct AppState {
     pub user_store: Model<UserStore>,
     pub workspace_store: Model<WorkspaceStore>,
     pub fs: Arc<dyn fs2::Fs>,
+    pub call_factory: CallFactory,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
     pub node_runtime: Arc<dyn NodeRuntime>,
@@ -322,6 +322,36 @@ struct Follower {
     peer_id: PeerId,
 }
 
+#[cfg(any(test, feature = "test-support"))]
+pub struct TestCallHandler;
+
+#[cfg(any(test, feature = "test-support"))]
+impl CallHandler for TestCallHandler {
+    fn peer_state(&mut self, id: PeerId, cx: &mut ViewContext<Workspace>) -> Option<(bool, bool)> {
+        None
+    }
+
+    fn shared_screen_for_peer(
+        &self,
+        peer_id: PeerId,
+        pane: &View<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Box<dyn ItemHandle>> {
+        None
+    }
+
+    fn room_id(&self, cx: &AppContext) -> Option<u64> {
+        None
+    }
+
+    fn hang_up(&self, cx: AsyncWindowContext) -> Result<Task<Result<()>>> {
+        anyhow::bail!("TestCallHandler should not be hanging up")
+    }
+
+    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
+        None
+    }
+}
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
@@ -352,6 +382,7 @@ impl AppState {
             workspace_store,
             node_runtime: FakeNodeRuntime::new(),
             build_window_options: |_, _, _| Default::default(),
+            call_factory: |_, _| Box::new(TestCallHandler),
         })
     }
 }
@@ -408,6 +439,23 @@ pub enum Event {
     WorkspaceCreated(WeakView<Workspace>),
 }
 
+#[async_trait(?Send)]
+pub trait CallHandler {
+    fn peer_state(&mut self, id: PeerId, cx: &mut ViewContext<Workspace>) -> Option<(bool, bool)>;
+    fn shared_screen_for_peer(
+        &self,
+        peer_id: PeerId,
+        pane: &View<Pane>,
+        cx: &mut ViewContext<Workspace>,
+    ) -> Option<Box<dyn ItemHandle>>;
+    fn room_id(&self, cx: &AppContext) -> Option<u64>;
+    fn is_in_room(&self, cx: &mut ViewContext<Workspace>) -> bool {
+        self.room_id(cx).is_some()
+    }
+    fn hang_up(&self, cx: AsyncWindowContext) -> Result<Task<Result<()>>>;
+    fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>>;
+}
+
 pub struct Workspace {
     window_self: WindowHandle<Self>,
     weak_self: WeakView<Self>,
@@ -428,10 +476,10 @@ pub struct Workspace {
     titlebar_item: Option<AnyView>,
     notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
     project: Model<Project>,
+    call_handler: Box<dyn CallHandler>,
     follower_states: HashMap<View<Pane>, FollowerState>,
     last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
     window_edited: bool,
-    active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
     app_state: Arc<AppState>,
@@ -459,6 +507,7 @@ struct FollowerState {
 
 enum WorkspaceBounds {}
 
+type CallFactory = fn(WeakView<Workspace>, &mut ViewContext<Workspace>) -> Box<dyn CallHandler>;
 impl Workspace {
     pub fn new(
         workspace_id: WorkspaceId,
@@ -550,9 +599,19 @@ impl Workspace {
             mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
         let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
             while let Some((leader_id, update)) = leader_updates_rx.next().await {
-                Self::process_leader_update(&this, leader_id, update, &mut cx)
+                let mut cx2 = cx.clone();
+                let t = this.clone();
+
+                Workspace::process_leader_update(&this, leader_id, update, &mut cx)
                     .await
                     .log_err();
+
+                // this.update(&mut cx, |this, cxx| {
+                //     this.call_handler
+                //         .process_leader_update(leader_id, update, cx2)
+                // })?
+                // .await
+                // .log_err();
             }
 
             Ok(())
@@ -585,14 +644,6 @@ impl Workspace {
         //     drag_and_drop.register_container(weak_handle.clone());
         // });
 
-        let mut active_call = None;
-        if cx.has_global::<Model<ActiveCall>>() {
-            let call = cx.global::<Model<ActiveCall>>().clone();
-            let mut subscriptions = Vec::new();
-            subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
-            active_call = Some((call, subscriptions));
-        }
-
         let subscriptions = vec![
             cx.observe_window_activation(Self::on_window_activation_changed),
             cx.observe_window_bounds(move |_, cx| {
@@ -655,7 +706,8 @@ impl Workspace {
             follower_states: Default::default(),
             last_leaders_by_pane: Default::default(),
             window_edited: false,
-            active_call,
+
+            call_handler: (app_state.call_factory)(weak_handle.clone(), cx),
             database_id: workspace_id,
             app_state,
             _observe_current_user,
@@ -1102,7 +1154,7 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         //todo!(saveing)
-        let active_call = self.active_call().cloned();
+
         let window = cx.window_handle();
 
         cx.spawn(|this, mut cx| async move {
@@ -1113,27 +1165,27 @@ impl Workspace {
                     .count()
             })?;
 
-            if let Some(active_call) = active_call {
-                if !quitting
-                    && workspace_count == 1
-                    && active_call.read_with(&cx, |call, _| call.room().is_some())?
-                {
-                    let answer = window.update(&mut cx, |_, cx| {
-                        cx.prompt(
-                            PromptLevel::Warning,
-                            "Do you want to leave the current call?",
-                            &["Close window and hang up", "Cancel"],
-                        )
-                    })?;
+            if !quitting
+                && workspace_count == 1
+                && this
+                    .update(&mut cx, |this, cx| this.call_handler.is_in_room(cx))
+                    .log_err()
+                    .unwrap_or_default()
+            {
+                let answer = window.update(&mut cx, |_, cx| {
+                    cx.prompt(
+                        PromptLevel::Warning,
+                        "Do you want to leave the current call?",
+                        &["Close window and hang up", "Cancel"],
+                    )
+                })?;
 
-                    if answer.await.log_err() == Some(1) {
-                        return anyhow::Ok(false);
-                    } else {
-                        active_call
-                            .update(&mut cx, |call, cx| call.hang_up(cx))?
-                            .await
-                            .log_err();
-                    }
+                if answer.await.log_err() == Some(1) {
+                    return anyhow::Ok(false);
+                } else {
+                    this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx.to_async()))??
+                        .await
+                        .log_err();
                 }
             }
 
@@ -2391,19 +2443,19 @@ impl Workspace {
     //     }
 
     pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
-        let state = self.follower_states.remove(pane)?;
+        let follower_states = &mut self.follower_states;
+        let state = follower_states.remove(pane)?;
         let leader_id = state.leader_id;
         for (_, item) in state.items_by_leader_view_id {
             item.set_leader_peer_id(None, cx);
         }
 
-        if self
-            .follower_states
+        if follower_states
             .values()
             .all(|state| state.leader_id != state.leader_id)
         {
             let project_id = self.project.read(cx).remote_id();
-            let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+            let room_id = self.call_handler.room_id(cx)?;
             self.app_state
                 .client
                 .send(proto::Unfollow {
@@ -2762,8 +2814,9 @@ impl Workspace {
         } else {
             None
         };
+        let room_id = self.call_handler.room_id(cx)?;
         self.app_state().workspace_store.update(cx, |store, cx| {
-            store.update_followers(project_id, update, cx)
+            store.update_followers(project_id, room_id, update, cx)
         })
     }
 
@@ -2771,31 +2824,12 @@ impl Workspace {
         self.follower_states.get(pane).map(|state| state.leader_id)
     }
 
-    fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+    pub fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
         cx.notify();
 
-        let call = self.active_call()?;
-        let room = call.read(cx).room()?.read(cx);
-        let participant = room.remote_participant_for_peer_id(leader_id)?;
+        let (leader_in_this_project, leader_in_this_app) =
+            self.call_handler.peer_state(leader_id, cx)?;
         let mut items_to_activate = Vec::new();
-
-        let leader_in_this_app;
-        let leader_in_this_project;
-        match participant.location {
-            call2::ParticipantLocation::SharedProject { project_id } => {
-                leader_in_this_app = true;
-                leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
-            }
-            call2::ParticipantLocation::UnsharedProject => {
-                leader_in_this_app = true;
-                leader_in_this_project = false;
-            }
-            call2::ParticipantLocation::External => {
-                leader_in_this_app = false;
-                leader_in_this_project = false;
-            }
-        };
-
         for (pane, state) in &self.follower_states {
             if state.leader_id != leader_id {
                 continue;
@@ -2825,8 +2859,8 @@ impl Workspace {
             if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
                 pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
             } else {
-                pane.update(cx, |pane, cx| {
-                    pane.add_item(item.boxed_clone(), false, false, None, cx)
+                pane.update(cx, |pane, mut cx| {
+                    pane.add_item(item.boxed_clone(), false, false, None, &mut cx)
                 });
             }
 
@@ -2886,25 +2920,6 @@ impl Workspace {
         }
     }
 
-    fn active_call(&self) -> Option<&Model<ActiveCall>> {
-        self.active_call.as_ref().map(|(call, _)| call)
-    }
-
-    fn on_active_call_event(
-        &mut self,
-        _: Model<ActiveCall>,
-        event: &call2::room::Event,
-        cx: &mut ViewContext<Self>,
-    ) {
-        match event {
-            call2::room::Event::ParticipantLocationChanged { participant_id }
-            | call2::room::Event::RemoteVideoTracksChanged { participant_id } => {
-                self.leader_updated(*participant_id, cx);
-            }
-            _ => {}
-        }
-    }
-
     pub fn database_id(&self) -> WorkspaceId {
         self.database_id
     }
@@ -3314,6 +3329,7 @@ impl Workspace {
             fs: project.read(cx).fs().clone(),
             build_window_options: |_, _, _| Default::default(),
             node_runtime: FakeNodeRuntime::new(),
+            call_factory: |_, _| Box::new(TestCallHandler),
         });
         let workspace = Self::new(0, project, app_state, cx);
         workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
@@ -3672,7 +3688,6 @@ impl Render for Workspace {
                                     .child(self.center.render(
                                         &self.project,
                                         &self.follower_states,
-                                        self.active_call(),
                                         &self.active_pane,
                                         self.zoomed.as_ref(),
                                         &self.app_state,
@@ -3842,14 +3857,10 @@ impl WorkspaceStore {
     pub fn update_followers(
         &self,
         project_id: Option<u64>,
+        room_id: u64,
         update: proto::update_followers::Variant,
         cx: &AppContext,
     ) -> Option<()> {
-        if !cx.has_global::<Model<ActiveCall>>() {
-            return None;
-        }
-
-        let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
         let follower_ids: Vec<_> = self
             .followers
             .iter()
@@ -3885,9 +3896,17 @@ impl WorkspaceStore {
                 project_id: envelope.payload.project_id,
                 peer_id: envelope.original_sender_id()?,
             };
-            let active_project = ActiveCall::global(cx).read(cx).location().cloned();
-
             let mut response = proto::FollowResponse::default();
+            let active_project = this
+                .workspaces
+                .iter()
+                .next()
+                .and_then(|workspace| {
+                    workspace
+                        .read_with(cx, |this, cx| this.call_handler.active_project(cx))
+                        .log_err()
+                })
+                .flatten();
             for workspace in &this.workspaces {
                 workspace
                     .update(cx, |workspace, cx| {

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 description = "The fast, collaborative code editor."
 edition = "2021"
 name = "zed"
-version = "0.114.0"
+version = "0.115.0"
 publish = false
 
 [lib]

crates/zed2/src/main.rs 🔗

@@ -180,6 +180,7 @@ fn main() {
             user_store,
             fs,
             build_window_options,
+            call_factory: call::Call::new,
             // background_actions: todo!("ask Mikayla"),
             workspace_store,
             node_runtime,
@@ -355,7 +356,6 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
             cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
                 .await
                 .log_err();
-
             // todo!(welcome)
             //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
             //todo!()