diff --git a/Cargo.lock b/Cargo.lock index e513df4e63..df785345e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1548,6 +1548,7 @@ dependencies = [ "log", "menu", "notifications", + "parking_lot 0.11.2", "picker", "postage", "pretty_assertions", @@ -9080,6 +9081,7 @@ dependencies = [ "nvim-rs", "parking_lot 0.11.2", "project", + "regex", "search", "serde", "serde_derive", 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/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 41d2c26f49..9bbf512877 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::*, UiTextSize}; @@ -31,6 +36,43 @@ 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)) + } + + 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 { pub fn new( language_registry: Arc, @@ -38,9 +80,11 @@ 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_use_autoclose(false); + editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this))); }); let buffer = editor @@ -150,6 +194,71 @@ impl MessageEditor { } } + fn completions( + &mut self, + buffer: &Model, + 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(), + }, + documentation: None, + server_id: LanguageServerId(0), // TODO: Make this optional or something? + lsp_completion: Default::default(), // TODO: Make this optional or something? + }) + .collect()) + }) + } + async fn find_mentions( this: WeakView, buffer: BufferSnapshot, 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)| { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e6a27bc109..96d3cafada 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}; @@ -365,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, @@ -732,85 +732,21 @@ impl CompletionsMenu { return None; } - let Some(project) = editor.project.clone() else { + let Some(provider) = editor.completion_provider.as_ref() else { return None; }; - let client = project.read(cx).client(); - let language_registry = project.read(cx).languages().clone(); + let resolve_task = provider.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( @@ -827,146 +763,16 @@ impl CompletionsMenu { let Some(project) = project else { return; }; - let language_registry = project.read(cx).languages().clone(); - let completions = self.completions.clone(); - let completions_guard = completions.read(); - let completion = &completions_guard[completion_index]; - if completion.documentation.is_some() { - return; - } - - let server_id = completion.server_id; - let completion = completion.lsp_completion.clone(); - drop(completions_guard); - - if project.read(cx).is_remote() { - let Some(project_id) = project.read(cx).remote_id() else { - log::error!("Remote project without remote_id"); - return; - }; - - let client = project.read(cx).client(); - - cx.spawn(move |this, mut cx| async move { - Self::resolve_completion_documentation_remote( - project_id, - server_id, - completions.clone(), - completion_index, - completion, - client, - language_registry.clone(), - ) - .await; - - _ = this.update(&mut cx, |_, cx| cx.notify()); - }) - .detach(); - } else { - let Some(server) = project.read(cx).language_server_for_id(server_id) else { - return; - }; - - cx.spawn(move |this, mut cx| async move { - Self::resolve_completion_documentation_local( - server, - completions, - completion_index, - completion, - language_registry, - ) - .await; - - _ = this.update(&mut cx, |_, cx| cx.notify()); - }) - .detach(); - } - } - - async fn resolve_completion_documentation_remote( - project_id: u64, - server_id: LanguageServerId, - completions: Arc>>, - 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 { @@ -1575,6 +1381,7 @@ impl Editor { ime_transaction: Default::default(), active_diagnostics: None, soft_wrap_mode_override, + completion_provider: project.clone().map(|project| Box::new(project) as _), collaboration_hub: project.clone().map(|project| Box::new(project) as _), project, blink_manager: blink_manager.clone(), @@ -1808,6 +1615,10 @@ impl Editor { self.collaboration_hub = Some(hub); } + pub fn set_completion_provider(&mut self, hub: Box) { + self.completion_provider = Some(hub); + } + pub fn placeholder_text(&self) -> Option<&str> { self.placeholder_text.as_deref() } @@ -3263,9 +3074,7 @@ impl Editor { return; } - let project = if let Some(project) = self.project.clone() { - project - } else { + let Some(provider) = self.completion_provider.as_ref() else { return; }; @@ -3281,9 +3090,7 @@ impl Editor { }; let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone()); - let completions = project.update(cx, |project, cx| { - project.completions(&buffer, buffer_position, cx) - }); + let completions = provider.completions(&buffer, buffer_position, cx); let id = post_inc(&mut self.next_completion_id); let task = cx.spawn(|this, mut cx| { @@ -3392,6 +3199,7 @@ impl Editor { let buffer_handle = completions_menu.buffer; let completions = completions_menu.completions.read(); let completion = completions.get(mat.candidate_id)?; + cx.stop_propagation(); let snippet; let text; @@ -3488,15 +3296,13 @@ impl Editor { this.refresh_copilot_suggestions(true, cx); }); - let project = self.project.clone()?; - let apply_edits = project.update(cx, |project, cx| { - project.apply_additional_edits_for_completion( - buffer_handle, - completion.clone(), - true, - cx, - ) - }); + let provider = self.completion_provider.as_ref()?; + let apply_edits = provider.apply_additional_edits_for_completion( + buffer_handle, + completion.clone(), + true, + cx, + ); Some(cx.foreground_executor().spawn(async move { apply_edits.await?; Ok(()) @@ -3918,7 +3724,7 @@ impl Editor { self.show_cursor_names = true; cx.notify(); cx.spawn(|this, mut cx| async move { - cx.background_executor().timer(Duration::from_secs(2)).await; + cx.background_executor().timer(Duration::from_secs(3)).await; this.update(&mut cx, |this, cx| { this.show_cursor_names = false; cx.notify() @@ -9108,6 +8914,66 @@ impl CollaborationHub for Model { } } +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>; + + fn apply_additional_edits_for_completion( + &self, + buffer: Model, + completion: Completion, + push_to_history: bool, + 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 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( location: Anchor, snapshot: &MultiBufferSnapshot, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1c4fafb268..4c6efcb02a 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; } @@ -2128,7 +2130,13 @@ impl EditorElement { if let Some(newest_selection_head) = newest_selection_head { if (start_row..end_row).contains(&newest_selection_head.row()) { if editor.context_menu_visible() { - let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.); + let max_height = cmp::min( + 12. * line_height, + cmp::max( + 3. * line_height, + (bounds.size.height - line_height) / 2., + ) + ); context_menu = editor.render_context_menu(newest_selection_head, &self.style, max_height, cx); } 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 ee7411bc4f..814b8cc788 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 { @@ -650,11 +648,11 @@ impl MacWindow { WindowBounds::Fixed(bounds) => { let display_bounds = display.bounds(); let frame = if bounds.intersects(&display_bounds) { - display_bounds_to_native(bounds) + global_bounds_to_ns_rect(bounds) } else { - display_bounds_to_native(display_bounds) + global_bounds_to_ns_rect(display_bounds) }; - native_window.setFrame_display_(mem::transmute::(frame), YES); + native_window.setFrame_display_(frame, YES); } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 7d44250a0f..59f8d79d84 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -379,8 +379,11 @@ pub trait LspAdapter: 'static + Send + Sync { #[derive(Clone, Debug, PartialEq, Eq)] pub struct CodeLabel { + /// The text to display. pub text: String, + /// Syntax highlighting runs. pub runs: Vec<(Range, HighlightId)>, + /// The portion of the text that should be used in fuzzy filtering. pub filter_range: Range, } 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, 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(); + } }) } 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"}} 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())), )