From 0479ebc26d6fb7812cc06d2b4ab8071ddd727078 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 19 Jan 2024 10:35:25 -0700 Subject: [PATCH 01/13] Don't toggle WHOLE_WORD in vim search Fixes */# in visual mode, and avoids setting up irritating state. --- Cargo.lock | 1 + assets/keymaps/vim.json | 16 +++++++-- crates/vim/Cargo.toml | 1 + crates/vim/README.md | 36 +++++++++++++++++++ crates/vim/src/normal/search.rs | 35 ++++++++++++------ .../vim/test_data/test_visual_star_hash.json | 6 ++++ 6 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 crates/vim/README.md create mode 100644 crates/vim/test_data/test_visual_star_hash.json diff --git a/Cargo.lock b/Cargo.lock index 010e7763e490515c7a47f3fada4ffa24a612e522..a9072004c3fda4143d50fa1c3c71118ee4ea471f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9079,6 +9079,7 @@ dependencies = [ "nvim-rs", "parking_lot 0.11.2", "project", + "regex", "search", "serde", "serde_derive", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 1da6f0ef8c5da25a60742fda933125c23eac81bf..32acb90d697837eecfda39efec54cb5f4420fbba 100644 --- a/assets/keymaps/vim.json +++ b/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": [ diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 95702585292d82fe586d72cc2e0bfb54f4979a1b..ef3fd2a4c790ee928ecc7ea05882fd7cf6d3a738 100644 --- a/crates/vim/Cargo.toml +++ b/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" } diff --git a/crates/vim/README.md b/crates/vim/README.md new file mode 100644 index 0000000000000000000000000000000000000000..547ca686fb9c5b5146d254db808a9b61aa630804 --- /dev/null +++ b/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. diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index f85e3d9ba92415040114fbcfd61c55c5066dbf79..31fda7788f01bbaf6799b5e6468c41236b12d025 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -91,7 +91,6 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext() { 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; + } } diff --git a/crates/vim/test_data/test_visual_star_hash.json b/crates/vim/test_data/test_visual_star_hash.json new file mode 100644 index 0000000000000000000000000000000000000000..d6523c4a45145cbbf1d053488227149c4fc99c3e --- /dev/null +++ b/crates/vim/test_data/test_visual_star_hash.json @@ -0,0 +1,6 @@ +{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} +{"Key":"v"} +{"Key":"3"} +{"Key":"l"} +{"Key":"*"} +{"Get":{"state":"a.c. abcd ˇa.c. abcd","mode":"Normal"}} From 00e46fdde09ea9a88a89da5fd05a83a85f84375a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 19 Jan 2024 12:51:59 -0700 Subject: [PATCH 02/13] Fix positioning of windows on secondary displays CGDisplayBounds returns data in "global display coordinates" (which are the same as Zed's coordinates), different from the NS APIs which use "screen coordinates" (which have the Y axis inverted) Also remove some transmutes while we're at it --- crates/gpui/src/platform/mac/display.rs | 45 +++++++++++++++---------- crates/gpui/src/platform/mac/window.rs | 16 ++++----- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/crates/gpui/src/platform/mac/display.rs b/crates/gpui/src/platform/mac/display.rs index 95ec83cd5a9d120fbcbe531a5cec3c9dafa3c95a..1f6023ed147364ca891a8b90f6db870e21420d51 100644 --- a/crates/gpui/src/platform/mac/display.rs +++ b/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 { +/// screen, with the Y axis pointing downwards (matching CoreGraphics) +pub(crate) fn global_bounds_from_ns_rect(rect: NSRect) -> Bounds { 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 { } } -/// 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) -> CGRect { +/// screen, with the Y axis pointing downwards (matching CoreGraphics) +pub(crate) fn global_bounds_to_ns_rect(bounds: Bounds) -> 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 { 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), + ), + } } } } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 134390bb79900b0cc09efba720b373303d8cd26b..e1277e3a09e7add9d3de60ac99a6ebd3796523d1 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/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 { - unsafe { - let frame = NSWindow::frame(self.native_window); - display_bounds_from_native(mem::transmute::(frame)) - } + let frame = unsafe { NSWindow::frame(self.native_window) }; + global_bounds_from_ns_rect(frame) } fn content_size(&self) -> Size { @@ -527,11 +525,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::(frame), YES); + native_window.setFrame_display_(frame, YES); } } From 1ceccdf03bd9a8749cf006ad8542c274338614bb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 19 Jan 2024 11:49:18 -0800 Subject: [PATCH 03/13] Move the details of completion-resolution logic into Project Co-authored-by: Conrad --- crates/editor/src/editor.rs | 239 ++++------------------------------ crates/project/src/project.rs | 174 ++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 221 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b31dd54208a52df335e7b1af55f163d758e91293..9a12827dbd5f14c7e6de56cbb5a63c107e03b3c4 100644 --- a/crates/editor/src/editor.rs +++ b/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}; @@ -735,81 +734,19 @@ impl CompletionsMenu { 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 = project.update(cx, |project, cx| { + project.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( @@ -826,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>>, - completion_index: usize, - completion: lsp::CompletionItem, - client: Arc, - language_registry: Arc, - ) { - 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, - completions: Arc>>, - completion_index: usize, - completion: lsp::CompletionItem, - language_registry: Arc, - ) { - 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::(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 { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2088fcbdaa1fc6cc8a744fcf987e4491d6ac05ca..4d4c6a7f8bf69ddca8c7a386e4fbdaaf5f693e28 100644 --- a/crates/project/src/project.rs +++ b/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, + completions: Arc>>, + cx: &mut ModelContext, + ) -> Task> { + 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, + completions: Arc>>, + completion_index: usize, + completion: lsp::CompletionItem, + language_registry: Arc, + ) { + 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::(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>>, + completion_index: usize, + completion: lsp::CompletionItem, + client: Arc, + language_registry: Arc, + ) { + 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, From 139986d080d3e9a8961e9d92ca9ad26d0bd4d3d8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 19 Jan 2024 13:12:33 -0800 Subject: [PATCH 04/13] Start work on autocomplete for chat mentions Co-authored-by: Conrad Co-authored-by: Nathan Co-authored-by: Marshall --- Cargo.lock | 1 + crates/collab_ui/Cargo.toml | 1 + .../src/chat_panel/message_editor.rs | 110 +++++++++++++++++- crates/editor/src/editor.rs | 67 ++++++++--- crates/editor/src/element.rs | 6 +- crates/language/src/language.rs | 2 + 6 files changed, 165 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 010e7763e490515c7a47f3fada4ffa24a612e522..00d254ef450194106e69216b58d2c00b0fc3f5d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1548,6 +1548,7 @@ dependencies = [ "log", "menu", "notifications", + "parking_lot 0.11.2", "picker", "postage", "pretty_assertions", diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 84c1810bc841d904a7a534fb562671dc9d7232c9..0fbf7deb7836c36be047a10f14cec419eadc5d8d 100644 --- a/crates/collab_ui/Cargo.toml +++ b/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 diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 7999db529a43985ae2b52cdde9f2108f9620b35c..05a9ad5c085e2272c4a4719789b0947bd96e44f1 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/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::*; @@ -31,6 +36,33 @@ pub struct MessageEditor { channel_id: Option, } +struct MessageEditorCompletionProvider(WeakView); + +impl CompletionProvider for MessageEditorCompletionProvider { + fn completions( + &self, + buffer: &Model, + buffer_position: language::Anchor, + cx: &mut ViewContext, + ) -> Task>> { + 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, + _completions: Arc>>, + _cx: &mut ViewContext, + ) -> Task> { + Task::ready(Ok(false)) + } +} + impl MessageEditor { pub fn new( language_registry: Arc, @@ -38,8 +70,10 @@ impl MessageEditor { editor: View, cx: &mut ViewContext, ) -> Self { + let this = cx.view().downgrade(); editor.update(cx, |editor, cx| { editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); + editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this))); }); let buffer = editor @@ -149,6 +183,71 @@ impl MessageEditor { } } + fn completions( + &mut self, + buffer: &Model, + end_anchor: Anchor, + cx: &mut ViewContext, + ) -> Task>> { + 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::()); + } + 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::>(); + 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(), + }, + server_id: LanguageServerId(0), // TODO: Make this optional or something? + documentation: None, + lsp_completion: Default::default(), // TODO: Make this optional or something? + }) + .collect()) + }) + } + async fn find_mentions( this: WeakView, buffer: BufferSnapshot, @@ -227,6 +326,7 @@ impl Render for MessageEditor { div() .w_full() + .h(px(500.)) .px_2() .py_1() .bg(cx.theme().colors().editor_background) @@ -260,7 +360,7 @@ mod tests { MessageEditor::new( language_registry, ChannelStore::global(cx), - cx.new_view(|cx| Editor::auto_height(4, cx)), + cx.new_view(|cx| Editor::auto_height(25, cx)), cx, ) }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9a12827dbd5f14c7e6de56cbb5a63c107e03b3c4..d716284efbc1ba73ff431b3f5129d4f73fdbb401 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -364,6 +364,7 @@ pub struct Editor { active_diagnostics: Option, soft_wrap_mode_override: Option, project: Option>, + completion_provider: Option>, collaboration_hub: Option>, blink_manager: Model, show_cursor_names: bool, @@ -730,17 +731,15 @@ impl CompletionsMenu { return None; } - let Some(project) = editor.project.clone() else { + let Some(provider) = editor.completion_provider.as_ref() else { return None; }; - let resolve_task = project.update(cx, |project, cx| { - project.resolve_completions( - self.matches.iter().map(|m| m.candidate_id).collect(), - self.completions.clone(), - cx, - ) - }); + let resolve_task = provider.resolve_completions( + self.matches.iter().map(|m| m.candidate_id).collect(), + self.completions.clone(), + cx, + ); return Some(cx.spawn(move |this, mut cx| async move { if let Some(true) = resolve_task.await.log_err() { @@ -1381,6 +1380,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(), @@ -1613,6 +1613,10 @@ impl Editor { self.collaboration_hub = Some(hub); } + pub fn set_completion_provider(&mut self, hub: Box) { + self.completion_provider = Some(hub); + } + pub fn placeholder_text(&self) -> Option<&str> { self.placeholder_text.as_deref() } @@ -3059,9 +3063,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; }; @@ -3077,9 +3079,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| { @@ -8904,6 +8904,45 @@ impl CollaborationHub for Model { } } +pub trait CompletionProvider { + fn completions( + &self, + buffer: &Model, + buffer_position: text::Anchor, + cx: &mut ViewContext, + ) -> Task>>; + fn resolve_completions( + &self, + completion_indices: Vec, + completions: Arc>>, + cx: &mut ViewContext, + ) -> Task>; +} + +impl CompletionProvider for Model { + fn completions( + &self, + buffer: &Model, + buffer_position: text::Anchor, + cx: &mut ViewContext, + ) -> Task>> { + self.update(cx, |project, cx| { + project.completions(&buffer, buffer_position, cx) + }) + } + + fn resolve_completions( + &self, + completion_indices: Vec, + completions: Arc>>, + cx: &mut ViewContext, + ) -> Task> { + self.update(cx, |project, cx| { + project.resolve_completions(completion_indices, completions, cx) + }) + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1c4fafb26898ec0ed05ed1ef9f5e98ce8b3fe3bd..c5aaf983d58579f45e4b8b193d04624dee40729a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1177,9 +1177,9 @@ impl EditorElement { list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); } - if list_origin.y + list_height > text_bounds.lower_right().y { - list_origin.y -= layout.position_map.line_height + list_height; - } + // if list_origin.y + list_height > text_bounds.lower_right().y { + // list_origin.y -= layout.position_map.line_height + list_height; + // } cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx)); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7d44250a0f21b289ff51b4ef88b589aed8bc62c3..ad283d90777b561e2d4173d206dc682a65f555df 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -380,7 +380,9 @@ pub trait LspAdapter: 'static + Send + Sync { #[derive(Clone, Debug, PartialEq, Eq)] pub struct CodeLabel { pub text: String, + /// Determines the syntax highlighting for the label pub runs: Vec<(Range, HighlightId)>, + /// Which part of the label participates pub filter_range: Range, } From af30a9b81418c136b840b208f93ddfd1634e58fa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 19 Jan 2024 14:38:17 -0700 Subject: [PATCH 05/13] Show cursors sligthly longer --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b31dd54208a52df335e7b1af55f163d758e91293..9c7e1e1f10f026856e4017980287fa3a6c5e4bf0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3907,7 +3907,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() From 7620feb8b2ca979a6bc7da451c0a1e66c0b66b43 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 19 Jan 2024 17:21:24 -0500 Subject: [PATCH 06/13] Prevent many call participants from overflowing the title bar (#4173) This PR fixes an issue where having a lot of participants in a call could cause the avatars/facepiles to overflow the title bar, pushing the call controls off-screen. The participant list will now scroll when it would otherwise exceed the available space: https://github.com/zed-industries/zed/assets/1486634/806c77e6-bd4c-4864-8567-92e0960734ee Release Notes: - Fixed participant list overflowing the title bar. --- crates/collab_ui/src/collab_titlebar_item.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 32ab64f77eedb869fdb25d538762ce1538d53715..43a749ec9536a4a420a81b17f7e00ffb94846998 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/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)| { From eaa0e93112b1174ac5bf07a51619b9bcdf9c3db3 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 19 Jan 2024 14:52:49 -0800 Subject: [PATCH 07/13] Fix hover popovers showing up over zoomed panels --- crates/editor/src/element.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1c4fafb26898ec0ed05ed1ef9f5e98ce8b3fe3bd..aed6c55668a1143d1076e950bae4b328a3aca786 100644 --- a/crates/editor/src/element.rs +++ b/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; } From 25f78a2ed14b8fbb9e3ddfaa6d1327dcdc43d3c7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 19 Jan 2024 15:02:10 -0800 Subject: [PATCH 08/13] Fix terminal selection firing when dragging anywhere --- crates/terminal/src/terminal.rs | 4 ++++ crates/terminal_view/src/terminal_element.rs | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 502706eb5b30bebe35507e1a9fddeb951d674c53..d9bdc17744810a59cb1c7b3303b1ddefc19e59ca 100644 --- a/crates/terminal/src/terminal.rs +++ b/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) }; diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 78235c3579624d510d47c64fd854ab46b82c8b96..29944b54d7729d7cf257c515339234db9f666751 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/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(); + } }) } From 24de848fdf2a32f14dbd36364d9901156b27f3d9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 19 Jan 2024 18:23:14 -0500 Subject: [PATCH 09/13] Prevent breadcrumbs from overflowing the toolbar (#4177) This PR prevents the breadcrumbs from overflowing the toolbar when its contents are long: Screenshot 2024-01-19 at 6 15 58 PM Release Notes: - Fixed an issue where long breadcrumbs would overflow the toolbar. --- crates/workspace/src/toolbar.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 3d5df3294e1fc84faab10c69c46c7ce424682aad..b127de8de5bea535658750e95bc181d5883cc1e2 100644 --- a/crates/workspace/src/toolbar.rs +++ b/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())), ) From 4fb3e6d812f854c56b64adf34d5f7ba04917e7ba Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 19 Jan 2024 15:47:42 -0800 Subject: [PATCH 10/13] Ensure editors context menus get at least 3 lines of height --- crates/collab_ui/src/chat_panel/message_editor.rs | 5 ++--- crates/editor/src/element.rs | 14 ++++++++++---- crates/language/src/language.rs | 5 +++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 05a9ad5c085e2272c4a4719789b0947bd96e44f1..c19d19085c97d9fd2e3702885c446faef19d7970 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -240,8 +240,8 @@ impl MessageEditor { text: format!("@{}", mat.string), runs: Vec::new(), }, - server_id: LanguageServerId(0), // TODO: Make this optional or something? documentation: None, + server_id: LanguageServerId(0), // TODO: Make this optional or something? lsp_completion: Default::default(), // TODO: Make this optional or something? }) .collect()) @@ -326,7 +326,6 @@ impl Render for MessageEditor { div() .w_full() - .h(px(500.)) .px_2() .py_1() .bg(cx.theme().colors().editor_background) @@ -360,7 +359,7 @@ mod tests { MessageEditor::new( language_registry, ChannelStore::global(cx), - cx.new_view(|cx| Editor::auto_height(25, cx)), + cx.new_view(|cx| Editor::auto_height(4, cx)), cx, ) }); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c5aaf983d58579f45e4b8b193d04624dee40729a..cb2eb4e49c13c9eb94e233ca26dbf5a3ee0ad1fd 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1177,9 +1177,9 @@ impl EditorElement { list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO); } - // if list_origin.y + list_height > text_bounds.lower_right().y { - // list_origin.y -= layout.position_map.line_height + list_height; - // } + if list_origin.y + list_height > text_bounds.lower_right().y { + list_origin.y -= layout.position_map.line_height + list_height; + } cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx)); } @@ -2128,7 +2128,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); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index ad283d90777b561e2d4173d206dc682a65f555df..59f8d79d84a3333fa2de06719b2559fa6baaed3a 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -379,10 +379,11 @@ pub trait LspAdapter: 'static + Send + Sync { #[derive(Clone, Debug, PartialEq, Eq)] pub struct CodeLabel { + /// The text to display. pub text: String, - /// Determines the syntax highlighting for the label + /// Syntax highlighting runs. pub runs: Vec<(Range, HighlightId)>, - /// Which part of the label participates + /// The portion of the text that should be used in fuzzy filtering. pub filter_range: Range, } From 739d1179e3fd6c24f4974e31794e42ed85ebe503 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 19 Jan 2024 16:02:51 -0800 Subject: [PATCH 11/13] Stop propagation when confirming a completion --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d716284efbc1ba73ff431b3f5129d4f73fdbb401..55090d75d95902bea93f1fe2fbb2687fa27c273e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3188,6 +3188,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; From c8adde32ded7283bfb8a5f62485ab903ef6e0a19 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 19 Jan 2024 16:03:08 -0800 Subject: [PATCH 12/13] Add shift-enter binding for newline in auto-height editors --- assets/keymaps/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index cd353d776749c6da998821ec20e68672e79aa835..8679296733559a03c51a94d1f133c6bd922eadf9 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -183,6 +183,7 @@ "context": "Editor && mode == auto_height", "bindings": { "ctrl-enter": "editor::Newline", + "shift-enter": "editor::Newline", "ctrl-shift-enter": "editor::NewlineBelow" } }, From 8fb0270b4a052c6ac48bf0356dee899d58b32c8a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 19 Jan 2024 16:56:28 -0800 Subject: [PATCH 13/13] Make applying of additional completion edits go through the CompletionProvider --- .../src/chat_panel/message_editor.rs | 10 +++++ crates/editor/src/editor.rs | 37 ++++++++++++++----- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index c19d19085c97d9fd2e3702885c446faef19d7970..48d3f31aa90ece128bbb22a206ef746706a2c977 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -61,6 +61,16 @@ impl CompletionProvider for MessageEditorCompletionProvider { ) -> Task> { Task::ready(Ok(false)) } + + fn apply_additional_edits_for_completion( + &self, + _buffer: Model, + _completion: Completion, + _push_to_history: bool, + _cx: &mut ViewContext, + ) -> Task>> { + Task::ready(Ok(None)) + } } impl MessageEditor { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 55090d75d95902bea93f1fe2fbb2687fa27c273e..47e4acdedf73b7f3e86ddd1b3837aeac80dfa417 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3285,15 +3285,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(()) @@ -8912,12 +8910,21 @@ pub trait CompletionProvider { buffer_position: text::Anchor, cx: &mut ViewContext, ) -> Task>>; + fn resolve_completions( &self, completion_indices: Vec, completions: Arc>>, cx: &mut ViewContext, ) -> Task>; + + fn apply_additional_edits_for_completion( + &self, + buffer: Model, + completion: Completion, + push_to_history: bool, + cx: &mut ViewContext, + ) -> Task>>; } impl CompletionProvider for Model { @@ -8942,6 +8949,18 @@ impl CompletionProvider for Model { project.resolve_completions(completion_indices, completions, cx) }) } + + fn apply_additional_edits_for_completion( + &self, + buffer: Model, + completion: Completion, + push_to_history: bool, + cx: &mut ViewContext, + ) -> Task>> { + self.update(cx, |project, cx| { + project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx) + }) + } } fn inlay_hint_settings(