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 010e7763e4..a9072004c3 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 1da6f0ef8c..32acb90d69 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 9570258529..ef3fd2a4c7 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 0000000000..547ca686fb --- /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 f85e3d9ba9..31fda7788f 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 0000000000..d6523c4a45 --- /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 95ec83cd5a..1f6023ed14 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 134390bb79..e1277e3a09 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 b31dd54208..9a12827dbd 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 resolve_task = project.update(cx, |project, cx| { + project.resolve_completions( + self.matches.iter().map(|m| m.candidate_id).collect(), + self.completions.clone(), + cx, + ) + }); - 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; - - _ = 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 2088fcbdaa..4d4c6a7f8b 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 010e7763e4..00d254ef45 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 84c1810bc8..0fbf7deb78 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 7999db529a..05a9ad5c08 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 9a12827dbd..d716284efb 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 1c4fafb268..c5aaf983d5 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 7d44250a0f..ad283d9077 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 b31dd54208..9c7e1e1f10 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 32ab64f77e..43a749ec95 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 1c4fafb268..aed6c55668 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 502706eb5b..d9bdc17744 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 78235c3579..29944b54d7 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 3d5df3294e..b127de8de5 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 05a9ad5c08..c19d19085c 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 c5aaf983d5..cb2eb4e49c 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 ad283d9077..59f8d79d84 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 d716284efb..55090d75d9 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 cd353d7767..8679296733 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 c19d19085c..48d3f31aa9 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 55090d75d9..47e4acdedf 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(