diff --git a/Cargo.lock b/Cargo.lock index a9072004c3..90b2ad5b78 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/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" } }, 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..48d3f31aa9 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,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,8 +80,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 +193,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/editor/src/editor.rs b/crates/editor/src/editor.rs index 9c7e1e1f10..4dc505a841 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, @@ -731,85 +731,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( @@ -826,146 +762,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 { @@ -1574,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(), @@ -1806,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() } @@ -3252,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; }; @@ -3270,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| { @@ -3381,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; @@ -3477,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(()) @@ -9097,6 +8903,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 aed6c55668..4c6efcb02a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2130,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/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,