Merge remote-tracking branch 'origin/main' into chat-font-size

Conrad Irwin created

Change summary

Cargo.lock                                        |   2 
assets/keymaps/vim.json                           |  16 
crates/collab_ui/Cargo.toml                       |   1 
crates/collab_ui/src/chat_panel/message_editor.rs | 117 +++++
crates/collab_ui/src/collab_titlebar_item.rs      |   9 
crates/editor/src/editor.rs                       | 332 +++++-----------
crates/editor/src/element.rs                      |  16 
crates/gpui/src/platform/mac/display.rs           |  45 +
crates/gpui/src/platform/mac/window.rs            |  16 
crates/language/src/language.rs                   |   3 
crates/project/src/project.rs                     | 174 ++++++++
crates/terminal/src/terminal.rs                   |   4 
crates/terminal_view/src/terminal_element.rs      |  12 
crates/vim/Cargo.toml                             |   1 
crates/vim/README.md                              |  36 +
crates/vim/src/normal/search.rs                   |  35 +
crates/vim/test_data/test_visual_star_hash.json   |   6 
crates/workspace/src/toolbar.rs                   |   8 
18 files changed, 543 insertions(+), 290 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -1548,6 +1548,7 @@ dependencies = [
  "log",
  "menu",
  "notifications",
+ "parking_lot 0.11.2",
  "picker",
  "postage",
  "pretty_assertions",
@@ -9080,6 +9081,7 @@ dependencies = [
  "nvim-rs",
  "parking_lot 0.11.2",
  "project",
+ "regex",
  "search",
  "serde",
  "serde_derive",

assets/keymaps/vim.json πŸ”—

@@ -104,8 +104,6 @@
       "shift-v": "vim::ToggleVisualLine",
       "ctrl-v": "vim::ToggleVisualBlock",
       "ctrl-q": "vim::ToggleVisualBlock",
-      "*": "vim::MoveToNext",
-      "#": "vim::MoveToPrev",
       "0": "vim::StartOfLine", // When no number operator present, use start of line motion
       "ctrl-f": "vim::PageDown",
       "pagedown": "vim::PageDown",
@@ -329,6 +327,8 @@
           "backwards": true
         }
       ],
+      "*": "vim::MoveToNext",
+      "#": "vim::MoveToPrev",
       ";": "vim::RepeatFind",
       ",": [
         "vim::RepeatFind",
@@ -421,6 +421,18 @@
       "shift-r": "vim::SubstituteLine",
       "c": "vim::Substitute",
       "~": "vim::ChangeCase",
+      "*": [
+        "vim::MoveToNext",
+        {
+          "partialWord": true
+        }
+      ],
+      "#": [
+        "vim::MoveToPrev",
+        {
+          "partialWord": true
+        }
+      ],
       "ctrl-a": "vim::Increment",
       "ctrl-x": "vim::Decrement",
       "g ctrl-a": [

crates/collab_ui/Cargo.toml πŸ”—

@@ -60,6 +60,7 @@ anyhow.workspace = true
 futures.workspace = true
 lazy_static.workspace = true
 log.workspace = true
+parking_lot.workspace = true
 schemars.workspace = true
 postage.workspace = true
 serde.workspace = true

crates/collab_ui/src/chat_panel/message_editor.rs πŸ”—

@@ -1,17 +1,22 @@
-use std::{sync::Arc, time::Duration};
-
+use anyhow::Result;
 use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
 use client::UserId;
 use collections::HashMap;
-use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
+use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
+use fuzzy::StringMatchCandidate;
 use gpui::{
     AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
     Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
 };
-use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
+use language::{
+    language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, Completion,
+    LanguageRegistry, LanguageServerId, ToOffset,
+};
 use lazy_static::lazy_static;
+use parking_lot::RwLock;
 use project::search::SearchQuery;
 use settings::Settings;
+use std::{sync::Arc, time::Duration};
 use theme::ThemeSettings;
 use ui::{prelude::*, UiTextSize};
 
@@ -31,6 +36,43 @@ pub struct MessageEditor {
     channel_id: Option<ChannelId>,
 }
 
+struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
+
+impl CompletionProvider for MessageEditorCompletionProvider {
+    fn completions(
+        &self,
+        buffer: &Model<Buffer>,
+        buffer_position: language::Anchor,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<anyhow::Result<Vec<language::Completion>>> {
+        let Some(handle) = self.0.upgrade() else {
+            return Task::ready(Ok(Vec::new()));
+        };
+        handle.update(cx, |message_editor, cx| {
+            message_editor.completions(buffer, buffer_position, cx)
+        })
+    }
+
+    fn resolve_completions(
+        &self,
+        _completion_indices: Vec<usize>,
+        _completions: Arc<RwLock<Box<[language::Completion]>>>,
+        _cx: &mut ViewContext<Editor>,
+    ) -> Task<anyhow::Result<bool>> {
+        Task::ready(Ok(false))
+    }
+
+    fn apply_additional_edits_for_completion(
+        &self,
+        _buffer: Model<Buffer>,
+        _completion: Completion,
+        _push_to_history: bool,
+        _cx: &mut ViewContext<Editor>,
+    ) -> Task<Result<Option<language::Transaction>>> {
+        Task::ready(Ok(None))
+    }
+}
+
 impl MessageEditor {
     pub fn new(
         language_registry: Arc<LanguageRegistry>,
@@ -38,9 +80,11 @@ impl MessageEditor {
         editor: View<Editor>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
+        let this = cx.view().downgrade();
         editor.update(cx, |editor, cx| {
             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
             editor.set_use_autoclose(false);
+            editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
         });
 
         let buffer = editor
@@ -150,6 +194,71 @@ impl MessageEditor {
         }
     }
 
+    fn completions(
+        &mut self,
+        buffer: &Model<Buffer>,
+        end_anchor: Anchor,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Vec<Completion>>> {
+        let end_offset = end_anchor.to_offset(buffer.read(cx));
+
+        let Some(query) = buffer.update(cx, |buffer, _| {
+            let mut query = String::new();
+            for ch in buffer.reversed_chars_at(end_offset).take(100) {
+                if ch == '@' {
+                    return Some(query.chars().rev().collect::<String>());
+                }
+                if ch.is_whitespace() || !ch.is_ascii() {
+                    break;
+                }
+                query.push(ch);
+            }
+            return None;
+        }) else {
+            return Task::ready(Ok(vec![]));
+        };
+
+        let start_offset = end_offset - query.len();
+        let start_anchor = buffer.read(cx).anchor_before(start_offset);
+
+        let candidates = self
+            .users
+            .keys()
+            .map(|user| StringMatchCandidate {
+                id: 0,
+                string: user.clone(),
+                char_bag: user.chars().collect(),
+            })
+            .collect::<Vec<_>>();
+        cx.spawn(|_, cx| async move {
+            let matches = fuzzy::match_strings(
+                &candidates,
+                &query,
+                true,
+                10,
+                &Default::default(),
+                cx.background_executor().clone(),
+            )
+            .await;
+
+            Ok(matches
+                .into_iter()
+                .map(|mat| Completion {
+                    old_range: start_anchor..end_anchor,
+                    new_text: mat.string.clone(),
+                    label: CodeLabel {
+                        filter_range: 1..mat.string.len() + 1,
+                        text: format!("@{}", mat.string),
+                        runs: Vec::new(),
+                    },
+                    documentation: None,
+                    server_id: LanguageServerId(0), // TODO: Make this optional or something?
+                    lsp_completion: Default::default(), // TODO: Make this optional or something?
+                })
+                .collect())
+        })
+    }
+
     async fn find_mentions(
         this: WeakView<MessageEditor>,
         buffer: BufferSnapshot,

crates/collab_ui/src/collab_titlebar_item.rs πŸ”—

@@ -85,7 +85,14 @@ impl Render for CollabTitlebarItem {
                     .gap_1()
                     .children(self.render_project_host(cx))
                     .child(self.render_project_name(cx))
-                    .child(div().pr_1().children(self.render_project_branch(cx)))
+                    .children(self.render_project_branch(cx)),
+            )
+            .child(
+                h_flex()
+                    .id("collaborator-list")
+                    .w_full()
+                    .gap_1()
+                    .overflow_x_scroll()
                     .when_some(
                         current_user.clone().zip(client.peer_id()).zip(room.clone()),
                         |this, ((current_user, peer_id), room)| {

crates/editor/src/editor.rs πŸ”—

@@ -40,7 +40,7 @@ pub(crate) use actions::*;
 use aho_corasick::AhoCorasick;
 use anyhow::{anyhow, Context as _, Result};
 use blink_manager::BlinkManager;
-use client::{Client, Collaborator, ParticipantIndex};
+use client::{Collaborator, ParticipantIndex};
 use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
@@ -71,8 +71,7 @@ use language::{
     language_settings::{self, all_language_settings, InlayHintSettings},
     markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
     CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
-    Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection,
-    SelectionGoal, TransactionId,
+    Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
 };
 
 use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
@@ -88,7 +87,7 @@ use ordered_float::OrderedFloat;
 use parking_lot::RwLock;
 use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
 use rand::prelude::*;
-use rpc::proto::{self, *};
+use rpc::proto::*;
 use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
@@ -365,6 +364,7 @@ pub struct Editor {
     active_diagnostics: Option<ActiveDiagnosticGroup>,
     soft_wrap_mode_override: Option<language_settings::SoftWrap>,
     project: Option<Model<Project>>,
+    completion_provider: Option<Box<dyn CompletionProvider>>,
     collaboration_hub: Option<Box<dyn CollaborationHub>>,
     blink_manager: Model<BlinkManager>,
     show_cursor_names: bool,
@@ -732,85 +732,21 @@ impl CompletionsMenu {
             return None;
         }
 
-        let Some(project) = editor.project.clone() else {
+        let Some(provider) = editor.completion_provider.as_ref() else {
             return None;
         };
 
-        let client = project.read(cx).client();
-        let language_registry = project.read(cx).languages().clone();
-
-        let is_remote = project.read(cx).is_remote();
-        let project_id = project.read(cx).remote_id();
-
-        let completions = self.completions.clone();
-        let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
-
-        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");
-                    return;
-                };
-
-                for completion_index in completion_indices {
-                    let completions_guard = completions.read();
-                    let completion = &completions_guard[completion_index];
-                    if completion.documentation.is_some() {
-                        continue;
-                    }
-
-                    let server_id = completion.server_id;
-                    let completion = completion.lsp_completion.clone();
-                    drop(completions_guard);
-
-                    Self::resolve_completion_documentation_remote(
-                        project_id,
-                        server_id,
-                        completions.clone(),
-                        completion_index,
-                        completion,
-                        client.clone(),
-                        language_registry.clone(),
-                    )
-                    .await;
-
-                    _ = this.update(&mut cx, |_, cx| cx.notify());
-                }
-            } else {
-                for completion_index in completion_indices {
-                    let completions_guard = completions.read();
-                    let completion = &completions_guard[completion_index];
-                    if completion.documentation.is_some() {
-                        continue;
-                    }
-
-                    let server_id = completion.server_id;
-                    let completion = completion.lsp_completion.clone();
-                    drop(completions_guard);
-
-                    let server = project
-                        .read_with(&mut cx, |project, _| {
-                            project.language_server_for_id(server_id)
-                        })
-                        .ok()
-                        .flatten();
-                    let Some(server) = server else {
-                        return;
-                    };
-
-                    Self::resolve_completion_documentation_local(
-                        server,
-                        completions.clone(),
-                        completion_index,
-                        completion,
-                        language_registry.clone(),
-                    )
-                    .await;
+        let resolve_task = provider.resolve_completions(
+            self.matches.iter().map(|m| m.candidate_id).collect(),
+            self.completions.clone(),
+            cx,
+        );
 
-                    _ = this.update(&mut cx, |_, cx| cx.notify());
-                }
+        return Some(cx.spawn(move |this, mut cx| async move {
+            if let Some(true) = resolve_task.await.log_err() {
+                this.update(&mut cx, |_, cx| cx.notify()).ok();
             }
-        }))
+        }));
     }
 
     fn attempt_resolve_selected_completion_documentation(
@@ -827,146 +763,16 @@ impl CompletionsMenu {
         let Some(project) = project else {
             return;
         };
-        let language_registry = project.read(cx).languages().clone();
-
-        let completions = self.completions.clone();
-        let completions_guard = completions.read();
-        let completion = &completions_guard[completion_index];
-        if completion.documentation.is_some() {
-            return;
-        }
-
-        let server_id = completion.server_id;
-        let completion = completion.lsp_completion.clone();
-        drop(completions_guard);
-
-        if project.read(cx).is_remote() {
-            let Some(project_id) = project.read(cx).remote_id() else {
-                log::error!("Remote project without remote_id");
-                return;
-            };
-
-            let client = project.read(cx).client();
-
-            cx.spawn(move |this, mut cx| async move {
-                Self::resolve_completion_documentation_remote(
-                    project_id,
-                    server_id,
-                    completions.clone(),
-                    completion_index,
-                    completion,
-                    client,
-                    language_registry.clone(),
-                )
-                .await;
-
-                _ = this.update(&mut cx, |_, cx| cx.notify());
-            })
-            .detach();
-        } else {
-            let Some(server) = project.read(cx).language_server_for_id(server_id) else {
-                return;
-            };
-
-            cx.spawn(move |this, mut cx| async move {
-                Self::resolve_completion_documentation_local(
-                    server,
-                    completions,
-                    completion_index,
-                    completion,
-                    language_registry,
-                )
-                .await;
-
-                _ = this.update(&mut cx, |_, cx| cx.notify());
-            })
-            .detach();
-        }
-    }
-
-    async fn resolve_completion_documentation_remote(
-        project_id: u64,
-        server_id: LanguageServerId,
-        completions: Arc<RwLock<Box<[Completion]>>>,
-        completion_index: usize,
-        completion: lsp::CompletionItem,
-        client: Arc<Client>,
-        language_registry: Arc<LanguageRegistry>,
-    ) {
-        let request = proto::ResolveCompletionDocumentation {
-            project_id,
-            language_server_id: server_id.0 as u64,
-            lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
-        };
-
-        let Some(response) = client
-            .request(request)
-            .await
-            .context("completion documentation resolve proto request")
-            .log_err()
-        else {
-            return;
-        };
-
-        if response.text.is_empty() {
-            let mut completions = completions.write();
-            let completion = &mut completions[completion_index];
-            completion.documentation = Some(Documentation::Undocumented);
-        }
-
-        let documentation = if response.is_markdown {
-            Documentation::MultiLineMarkdown(
-                markdown::parse_markdown(&response.text, &language_registry, None).await,
-            )
-        } else if response.text.lines().count() <= 1 {
-            Documentation::SingleLine(response.text)
-        } else {
-            Documentation::MultiLinePlainText(response.text)
-        };
-
-        let mut completions = completions.write();
-        let completion = &mut completions[completion_index];
-        completion.documentation = Some(documentation);
-    }
-
-    async fn resolve_completion_documentation_local(
-        server: Arc<lsp::LanguageServer>,
-        completions: Arc<RwLock<Box<[Completion]>>>,
-        completion_index: usize,
-        completion: lsp::CompletionItem,
-        language_registry: Arc<LanguageRegistry>,
-    ) {
-        let can_resolve = server
-            .capabilities()
-            .completion_provider
-            .as_ref()
-            .and_then(|options| options.resolve_provider)
-            .unwrap_or(false);
-        if !can_resolve {
-            return;
-        }
-
-        let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
-        let Some(completion_item) = request.await.log_err() else {
-            return;
-        };
-
-        if let Some(lsp_documentation) = completion_item.documentation {
-            let documentation = language::prepare_completion_documentation(
-                &lsp_documentation,
-                &language_registry,
-                None, // TODO: Try to reasonably work out which language the completion is for
-            )
-            .await;
 
-            let mut completions = completions.write();
-            let completion = &mut completions[completion_index];
-            completion.documentation = Some(documentation);
-        } else {
-            let mut completions = completions.write();
-            let completion = &mut completions[completion_index];
-            completion.documentation = Some(Documentation::Undocumented);
-        }
+        let resolve_task = project.update(cx, |project, cx| {
+            project.resolve_completions(vec![completion_index], self.completions.clone(), cx)
+        });
+        cx.spawn(move |this, mut cx| async move {
+            if let Some(true) = resolve_task.await.log_err() {
+                this.update(&mut cx, |_, cx| cx.notify()).ok();
+            }
+        })
+        .detach();
     }
 
     fn visible(&self) -> bool {
@@ -1575,6 +1381,7 @@ impl Editor {
             ime_transaction: Default::default(),
             active_diagnostics: None,
             soft_wrap_mode_override,
+            completion_provider: project.clone().map(|project| Box::new(project) as _),
             collaboration_hub: project.clone().map(|project| Box::new(project) as _),
             project,
             blink_manager: blink_manager.clone(),
@@ -1808,6 +1615,10 @@ impl Editor {
         self.collaboration_hub = Some(hub);
     }
 
+    pub fn set_completion_provider(&mut self, hub: Box<dyn CompletionProvider>) {
+        self.completion_provider = Some(hub);
+    }
+
     pub fn placeholder_text(&self) -> Option<&str> {
         self.placeholder_text.as_deref()
     }
@@ -3263,9 +3074,7 @@ impl Editor {
             return;
         }
 
-        let project = if let Some(project) = self.project.clone() {
-            project
-        } else {
+        let Some(provider) = self.completion_provider.as_ref() else {
             return;
         };
 
@@ -3281,9 +3090,7 @@ impl Editor {
         };
 
         let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone());
-        let completions = project.update(cx, |project, cx| {
-            project.completions(&buffer, buffer_position, cx)
-        });
+        let completions = provider.completions(&buffer, buffer_position, cx);
 
         let id = post_inc(&mut self.next_completion_id);
         let task = cx.spawn(|this, mut cx| {
@@ -3392,6 +3199,7 @@ impl Editor {
         let buffer_handle = completions_menu.buffer;
         let completions = completions_menu.completions.read();
         let completion = completions.get(mat.candidate_id)?;
+        cx.stop_propagation();
 
         let snippet;
         let text;
@@ -3488,15 +3296,13 @@ impl Editor {
             this.refresh_copilot_suggestions(true, cx);
         });
 
-        let project = self.project.clone()?;
-        let apply_edits = project.update(cx, |project, cx| {
-            project.apply_additional_edits_for_completion(
-                buffer_handle,
-                completion.clone(),
-                true,
-                cx,
-            )
-        });
+        let provider = self.completion_provider.as_ref()?;
+        let apply_edits = provider.apply_additional_edits_for_completion(
+            buffer_handle,
+            completion.clone(),
+            true,
+            cx,
+        );
         Some(cx.foreground_executor().spawn(async move {
             apply_edits.await?;
             Ok(())
@@ -3918,7 +3724,7 @@ impl Editor {
         self.show_cursor_names = true;
         cx.notify();
         cx.spawn(|this, mut cx| async move {
-            cx.background_executor().timer(Duration::from_secs(2)).await;
+            cx.background_executor().timer(Duration::from_secs(3)).await;
             this.update(&mut cx, |this, cx| {
                 this.show_cursor_names = false;
                 cx.notify()
@@ -9108,6 +8914,66 @@ impl CollaborationHub for Model<Project> {
     }
 }
 
+pub trait CompletionProvider {
+    fn completions(
+        &self,
+        buffer: &Model<Buffer>,
+        buffer_position: text::Anchor,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<Result<Vec<Completion>>>;
+
+    fn resolve_completions(
+        &self,
+        completion_indices: Vec<usize>,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<Result<bool>>;
+
+    fn apply_additional_edits_for_completion(
+        &self,
+        buffer: Model<Buffer>,
+        completion: Completion,
+        push_to_history: bool,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<Result<Option<language::Transaction>>>;
+}
+
+impl CompletionProvider for Model<Project> {
+    fn completions(
+        &self,
+        buffer: &Model<Buffer>,
+        buffer_position: text::Anchor,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<Result<Vec<Completion>>> {
+        self.update(cx, |project, cx| {
+            project.completions(&buffer, buffer_position, cx)
+        })
+    }
+
+    fn resolve_completions(
+        &self,
+        completion_indices: Vec<usize>,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<Result<bool>> {
+        self.update(cx, |project, cx| {
+            project.resolve_completions(completion_indices, completions, cx)
+        })
+    }
+
+    fn apply_additional_edits_for_completion(
+        &self,
+        buffer: Model<Buffer>,
+        completion: Completion,
+        push_to_history: bool,
+        cx: &mut ViewContext<Editor>,
+    ) -> Task<Result<Option<language::Transaction>>> {
+        self.update(cx, |project, cx| {
+            project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
+        })
+    }
+}
+
 fn inlay_hint_settings(
     location: Anchor,
     snapshot: &MultiBufferSnapshot,

crates/editor/src/element.rs πŸ”—

@@ -1218,9 +1218,11 @@ impl EditorElement {
                         popover_origin.x = popover_origin.x + x_out_of_bounds;
                     }
 
-                    cx.break_content_mask(|cx| {
-                        hover_popover.draw(popover_origin, available_space, cx)
-                    });
+                    if cx.was_top_layer(&popover_origin, cx.stacking_order()) {
+                        cx.break_content_mask(|cx| {
+                            hover_popover.draw(popover_origin, available_space, cx)
+                        });
+                    }
 
                     current_y = popover_origin.y - HOVER_POPOVER_GAP;
                 }
@@ -2128,7 +2130,13 @@ impl EditorElement {
             if let Some(newest_selection_head) = newest_selection_head {
                 if (start_row..end_row).contains(&newest_selection_head.row()) {
                     if editor.context_menu_visible() {
-                        let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.);
+                        let max_height = cmp::min(
+                            12. * line_height,
+                            cmp::max(
+                                3. * line_height,
+                                (bounds.size.height - line_height) / 2.,
+                            )
+                        );
                         context_menu =
                             editor.render_context_menu(newest_selection_head, &self.style, max_height, cx);
                     }

crates/gpui/src/platform/mac/display.rs πŸ”—

@@ -3,13 +3,10 @@ use anyhow::Result;
 use cocoa::{
     appkit::NSScreen,
     base::{id, nil},
-    foundation::{NSDictionary, NSString},
+    foundation::{NSDictionary, NSPoint, NSRect, NSSize, NSString},
 };
 use core_foundation::uuid::{CFUUIDGetUUIDBytes, CFUUIDRef};
-use core_graphics::{
-    display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList},
-    geometry::{CGPoint, CGRect, CGSize},
-};
+use core_graphics::display::{CGDirectDisplayID, CGDisplayBounds, CGGetActiveDisplayList};
 use objc::{msg_send, sel, sel_impl};
 use uuid::Uuid;
 
@@ -77,14 +74,14 @@ extern "C" {
     fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
 }
 
-/// Convert the given rectangle from CoreGraphics' native coordinate space to GPUI's coordinate space.
+/// Convert the given rectangle from Cocoa's coordinate space to GPUI's coordinate space.
 ///
-/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
+/// Cocoa's coordinate space has its origin at the bottom left of the primary screen,
 /// with the Y axis pointing upwards.
 ///
 /// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
-/// screen, with the Y axis pointing downwards.
-pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> {
+/// screen, with the Y axis pointing downwards (matching CoreGraphics)
+pub(crate) fn global_bounds_from_ns_rect(rect: NSRect) -> Bounds<GlobalPixels> {
     let primary_screen_size = unsafe { CGDisplayBounds(MacDisplay::primary().id().0) }.size;
 
     Bounds {
@@ -101,22 +98,22 @@ pub(crate) fn display_bounds_from_native(rect: CGRect) -> Bounds<GlobalPixels> {
     }
 }
 
-/// Convert the given rectangle from GPUI's coordinate system to CoreGraphics' native coordinate space.
+/// Convert the given rectangle from GPUI's coordinate system to Cocoa's native coordinate space.
 ///
-/// CoreGraphics' coordinate space has its origin at the bottom left of the primary screen,
+/// Cocoa's coordinate space has its origin at the bottom left of the primary screen,
 /// with the Y axis pointing upwards.
 ///
 /// Conversely, in GPUI's coordinate system, the origin is placed at the top left of the primary
-/// screen, with the Y axis pointing downwards.
-pub(crate) fn display_bounds_to_native(bounds: Bounds<GlobalPixels>) -> CGRect {
+/// screen, with the Y axis pointing downwards (matching CoreGraphics)
+pub(crate) fn global_bounds_to_ns_rect(bounds: Bounds<GlobalPixels>) -> NSRect {
     let primary_screen_height = MacDisplay::primary().bounds().size.height;
 
-    CGRect::new(
-        &CGPoint::new(
+    NSRect::new(
+        NSPoint::new(
             bounds.origin.x.into(),
             (primary_screen_height - bounds.origin.y - bounds.size.height).into(),
         ),
-        &CGSize::new(bounds.size.width.into(), bounds.size.height.into()),
+        NSSize::new(bounds.size.width.into(), bounds.size.height.into()),
     )
 }
 
@@ -155,8 +152,20 @@ impl PlatformDisplay for MacDisplay {
 
     fn bounds(&self) -> Bounds<GlobalPixels> {
         unsafe {
-            let native_bounds = CGDisplayBounds(self.0);
-            display_bounds_from_native(native_bounds)
+            // CGDisplayBounds is in "global display" coordinates, where 0 is
+            // the top left of the primary display.
+            let bounds = CGDisplayBounds(self.0);
+
+            Bounds {
+                origin: point(
+                    GlobalPixels(bounds.origin.x as f32),
+                    GlobalPixels(bounds.origin.y as f32),
+                ),
+                size: size(
+                    GlobalPixels(bounds.size.width as f32),
+                    GlobalPixels(bounds.size.height as f32),
+                ),
+            }
         }
     }
 }

crates/gpui/src/platform/mac/window.rs πŸ”—

@@ -1,6 +1,6 @@
-use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange};
+use super::{global_bounds_from_ns_rect, ns_string, MacDisplay, MetalRenderer, NSRange};
 use crate::{
-    display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
+    global_bounds_to_ns_rect, point, px, size, AnyWindowHandle, Bounds, ExternalPaths,
     FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers,
     ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
     PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
@@ -411,10 +411,8 @@ impl MacWindowState {
     }
 
     fn frame(&self) -> Bounds<GlobalPixels> {
-        unsafe {
-            let frame = NSWindow::frame(self.native_window);
-            display_bounds_from_native(mem::transmute::<NSRect, CGRect>(frame))
-        }
+        let frame = unsafe { NSWindow::frame(self.native_window) };
+        global_bounds_from_ns_rect(frame)
     }
 
     fn content_size(&self) -> Size<Pixels> {
@@ -650,11 +648,11 @@ impl MacWindow {
                 WindowBounds::Fixed(bounds) => {
                     let display_bounds = display.bounds();
                     let frame = if bounds.intersects(&display_bounds) {
-                        display_bounds_to_native(bounds)
+                        global_bounds_to_ns_rect(bounds)
                     } else {
-                        display_bounds_to_native(display_bounds)
+                        global_bounds_to_ns_rect(display_bounds)
                     };
-                    native_window.setFrame_display_(mem::transmute::<CGRect, NSRect>(frame), YES);
+                    native_window.setFrame_display_(frame, YES);
                 }
             }
 

crates/language/src/language.rs πŸ”—

@@ -379,8 +379,11 @@ pub trait LspAdapter: 'static + Send + Sync {
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct CodeLabel {
+    /// The text to display.
     pub text: String,
+    /// Syntax highlighting runs.
     pub runs: Vec<(Range<usize>, HighlightId)>,
+    /// The portion of the text that should be used in fuzzy filtering.
     pub filter_range: Range<usize>,
 }
 

crates/project/src/project.rs πŸ”—

@@ -34,16 +34,16 @@ use gpui::{
 use itertools::Itertools;
 use language::{
     language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
-    point_to_lsp,
+    markdown, point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version, split_operations,
     },
     range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
     CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
-    Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
-    LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
-    TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
+    Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName,
+    LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer,
+    PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
 use log::error;
 use lsp::{
@@ -52,7 +52,7 @@ use lsp::{
 };
 use lsp_command::*;
 use node_runtime::NodeRuntime;
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use postage::watch;
 use prettier_support::{DefaultPrettier, PrettierInstance};
 use project_settings::{LspSettings, ProjectSettings};
@@ -4828,6 +4828,170 @@ impl Project {
         }
     }
 
+    pub fn resolve_completions(
+        &self,
+        completion_indices: Vec<usize>,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<bool>> {
+        let client = self.client();
+        let language_registry = self.languages().clone();
+
+        let is_remote = self.is_remote();
+        let project_id = self.remote_id();
+
+        cx.spawn(move |this, mut cx| async move {
+            let mut did_resolve = false;
+            if is_remote {
+                let project_id =
+                    project_id.ok_or_else(|| anyhow!("Remote project without remote_id"))?;
+
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
+
+                    did_resolve = true;
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    Self::resolve_completion_documentation_remote(
+                        project_id,
+                        server_id,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        client.clone(),
+                        language_registry.clone(),
+                    )
+                    .await;
+                }
+            } else {
+                for completion_index in completion_indices {
+                    let completions_guard = completions.read();
+                    let completion = &completions_guard[completion_index];
+                    if completion.documentation.is_some() {
+                        continue;
+                    }
+
+                    let server_id = completion.server_id;
+                    let completion = completion.lsp_completion.clone();
+                    drop(completions_guard);
+
+                    let server = this
+                        .read_with(&mut cx, |project, _| {
+                            project.language_server_for_id(server_id)
+                        })
+                        .ok()
+                        .flatten();
+                    let Some(server) = server else {
+                        continue;
+                    };
+
+                    did_resolve = true;
+                    Self::resolve_completion_documentation_local(
+                        server,
+                        completions.clone(),
+                        completion_index,
+                        completion,
+                        language_registry.clone(),
+                    )
+                    .await;
+                }
+            }
+
+            Ok(did_resolve)
+        })
+    }
+
+    async fn resolve_completion_documentation_local(
+        server: Arc<lsp::LanguageServer>,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        completion_index: usize,
+        completion: lsp::CompletionItem,
+        language_registry: Arc<LanguageRegistry>,
+    ) {
+        let can_resolve = server
+            .capabilities()
+            .completion_provider
+            .as_ref()
+            .and_then(|options| options.resolve_provider)
+            .unwrap_or(false);
+        if !can_resolve {
+            return;
+        }
+
+        let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
+        let Some(completion_item) = request.await.log_err() else {
+            return;
+        };
+
+        if let Some(lsp_documentation) = completion_item.documentation {
+            let documentation = language::prepare_completion_documentation(
+                &lsp_documentation,
+                &language_registry,
+                None, // TODO: Try to reasonably work out which language the completion is for
+            )
+            .await;
+
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(documentation);
+        } else {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
+    }
+
+    async fn resolve_completion_documentation_remote(
+        project_id: u64,
+        server_id: LanguageServerId,
+        completions: Arc<RwLock<Box<[Completion]>>>,
+        completion_index: usize,
+        completion: lsp::CompletionItem,
+        client: Arc<Client>,
+        language_registry: Arc<LanguageRegistry>,
+    ) {
+        let request = proto::ResolveCompletionDocumentation {
+            project_id,
+            language_server_id: server_id.0 as u64,
+            lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
+        };
+
+        let Some(response) = client
+            .request(request)
+            .await
+            .context("completion documentation resolve proto request")
+            .log_err()
+        else {
+            return;
+        };
+
+        if response.text.is_empty() {
+            let mut completions = completions.write();
+            let completion = &mut completions[completion_index];
+            completion.documentation = Some(Documentation::Undocumented);
+        }
+
+        let documentation = if response.is_markdown {
+            Documentation::MultiLineMarkdown(
+                markdown::parse_markdown(&response.text, &language_registry, None).await,
+            )
+        } else if response.text.lines().count() <= 1 {
+            Documentation::SingleLine(response.text)
+        } else {
+            Documentation::MultiLinePlainText(response.text)
+        };
+
+        let mut completions = completions.write();
+        let completion = &mut completions[completion_index];
+        completion.documentation = Some(documentation);
+    }
+
     pub fn apply_additional_edits_for_completion(
         &self,
         buffer_handle: Model<Buffer>,

crates/terminal/src/terminal.rs πŸ”—

@@ -599,6 +599,10 @@ impl Terminal {
         }
     }
 
+    pub fn selection_started(&self) -> bool {
+        self.selection_phase == SelectionPhase::Selecting
+    }
+
     /// Updates the cached process info, returns whether the Zed-relevant info has changed
     fn update_process_info(&mut self) -> bool {
         let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };

crates/terminal_view/src/terminal_element.rs πŸ”—

@@ -621,9 +621,17 @@ impl TerminalElement {
                 }
 
                 if e.pressed_button.is_some() && !cx.has_active_drag() {
+                    let visibly_contains = interactive_bounds.visibly_contains(&e.position, cx);
                     terminal.update(cx, |terminal, cx| {
-                        terminal.mouse_drag(e, origin, bounds);
-                        cx.notify();
+                        if !terminal.selection_started() {
+                            if visibly_contains {
+                                terminal.mouse_drag(e, origin, bounds);
+                                cx.notify();
+                            }
+                        } else {
+                            terminal.mouse_drag(e, origin, bounds);
+                            cx.notify();
+                        }
                     })
                 }
 

crates/vim/Cargo.toml πŸ”—

@@ -23,6 +23,7 @@ async-trait = { workspace = true, "optional" = true }
 nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
 tokio = { version = "1.15", "optional" = true }
 serde_json.workspace = true
+regex.workspace = true
 
 collections = { path = "../collections" }
 command_palette = { path = "../command_palette" }

crates/vim/README.md πŸ”—

@@ -0,0 +1,36 @@
+This contains the code for Zed's Vim emulation mode.
+
+Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. This means Zed will never be 100% vim compatible, but should be 100% vim familiar!
+
+The backlog is maintained in the `#vim` channel notes.
+
+## Testing against Neovim
+
+If you are making a change to make Zed's behaviour more closely match vim/nvim, you can create a test using the `NeovimBackedTestContext`.
+
+For example, the following test checks that Zed and Neovim have the same behaviour when running `*` in visual mode:
+
+```rust
+#[gpui::test]
+async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
+    let mut cx = NeovimBackedTestContext::new(cx).await;
+
+    cx.set_shared_state("Λ‡a.c. abcd a.c. abcd").await;
+    cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await;
+    cx.assert_shared_state("a.c. abcd Λ‡a.c. abcd").await;
+}
+```
+
+To keep CI runs fast, by default the neovim tests use a cached JSON file that records what neovim did (see crates/vim/test_data),
+but while developing this test you'll need to run it with the neovim flag enabled:
+
+```
+cargo test -p vim --features neovim test_visual_star_hash
+```
+
+This will run your keystrokes against a headless neovim and cache the results in the test_data directory.
+
+
+## Testing zed-only behaviour
+
+Zed does more than vim/neovim in their default modes. The `VimTestContext` can be used instead. This lets you test integration with the language server and other parts of zed's UI that don't have a NeoVim equivalent.

crates/vim/src/normal/search.rs πŸ”—

@@ -91,7 +91,6 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
 
                     if query.is_empty() {
                         search_bar.set_replacement(None, cx);
-                        search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
                         search_bar.activate_search_mode(SearchMode::Regex, cx);
                     }
                     vim.workspace_state.search = SearchState {
@@ -149,15 +148,19 @@ pub fn move_to_internal(
         pane.update(cx, |pane, cx| {
             if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
                 let search = search_bar.update(cx, |search_bar, cx| {
-                    let mut options = SearchOptions::CASE_SENSITIVE;
-                    options.set(SearchOptions::WHOLE_WORD, whole_word);
-                    if search_bar.show(cx) {
-                        search_bar
-                            .query_suggestion(cx)
-                            .map(|query| search_bar.search(&query, Some(options), cx))
-                    } else {
-                        None
+                    let options = SearchOptions::CASE_SENSITIVE;
+                    if !search_bar.show(cx) {
+                        return None;
+                    }
+                    let Some(query) = search_bar.query_suggestion(cx) else {
+                        return None;
+                    };
+                    let mut query = regex::escape(&query);
+                    if whole_word {
+                        query = format!(r"\b{}\b", query);
                     }
+                    search_bar.activate_search_mode(SearchMode::Regex, cx);
+                    Some(search_bar.search(&query, Some(options), cx))
                 });
 
                 if let Some(search) = search {
@@ -350,7 +353,10 @@ mod test {
     use editor::DisplayPoint;
     use search::BufferSearchBar;
 
-    use crate::{state::Mode, test::VimTestContext};
+    use crate::{
+        state::Mode,
+        test::{NeovimBackedTestContext, VimTestContext},
+    };
 
     #[gpui::test]
     async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
@@ -474,4 +480,13 @@ mod test {
         cx.simulate_keystrokes(["shift-enter"]);
         cx.assert_editor_state("«oneˇ» one one one");
     }
+
+    #[gpui::test]
+    async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
+        let mut cx = NeovimBackedTestContext::new(cx).await;
+
+        cx.set_shared_state("Λ‡a.c. abcd a.c. abcd").await;
+        cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await;
+        cx.assert_shared_state("a.c. abcd Λ‡a.c. abcd").await;
+    }
 }

crates/workspace/src/toolbar.rs πŸ”—

@@ -112,18 +112,22 @@ impl Render for Toolbar {
             .child(
                 h_flex()
                     .justify_between()
+                    .gap_2()
                     .when(has_left_items, |this| {
                         this.child(
                             h_flex()
-                                .flex_1()
+                                .flex_auto()
                                 .justify_start()
+                                .overflow_x_hidden()
                                 .children(self.left_items().map(|item| item.to_any())),
                         )
                     })
                     .when(has_right_items, |this| {
                         this.child(
                             h_flex()
-                                .flex_1()
+                                // We're using `flex_none` here to prevent some flickering that can occur when the
+                                // size of the left items container changes.
+                                .flex_none()
                                 .justify_end()
                                 .children(self.right_items().map(|item| item.to_any())),
                         )