Cargo.lock 🔗
@@ -1548,6 +1548,7 @@ dependencies = [
"log",
"menu",
"notifications",
+ "parking_lot 0.11.2",
"picker",
"postage",
"pretty_assertions",
Max Brunsfeld , Conrad , Nathan , and Marshall created
Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>
Cargo.lock | 1
crates/collab_ui/Cargo.toml | 1
crates/collab_ui/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(-)
@@ -1548,6 +1548,7 @@ dependencies = [
"log",
"menu",
"notifications",
+ "parking_lot 0.11.2",
"picker",
"postage",
"pretty_assertions",
@@ -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
@@ -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<ChannelId>,
}
+struct MessageEditorCompletionProvider(WeakView<MessageEditor>);
+
+impl CompletionProvider for MessageEditorCompletionProvider {
+ fn completions(
+ &self,
+ buffer: &Model<Buffer>,
+ buffer_position: language::Anchor,
+ cx: &mut ViewContext<Editor>,
+ ) -> Task<anyhow::Result<Vec<language::Completion>>> {
+ let Some(handle) = self.0.upgrade() else {
+ return Task::ready(Ok(Vec::new()));
+ };
+ handle.update(cx, |message_editor, cx| {
+ message_editor.completions(buffer, buffer_position, cx)
+ })
+ }
+
+ fn resolve_completions(
+ &self,
+ _completion_indices: Vec<usize>,
+ _completions: Arc<RwLock<Box<[language::Completion]>>>,
+ _cx: &mut ViewContext<Editor>,
+ ) -> Task<anyhow::Result<bool>> {
+ Task::ready(Ok(false))
+ }
+}
+
impl MessageEditor {
pub fn new(
language_registry: Arc<LanguageRegistry>,
@@ -38,8 +70,10 @@ impl MessageEditor {
editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
+ let this = cx.view().downgrade();
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
+ editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
});
let buffer = editor
@@ -149,6 +183,71 @@ impl MessageEditor {
}
}
+ fn completions(
+ &mut self,
+ buffer: &Model<Buffer>,
+ end_anchor: Anchor,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<Vec<Completion>>> {
+ let end_offset = end_anchor.to_offset(buffer.read(cx));
+
+ let Some(query) = buffer.update(cx, |buffer, _| {
+ let mut query = String::new();
+ for ch in buffer.reversed_chars_at(end_offset).take(100) {
+ if ch == '@' {
+ return Some(query.chars().rev().collect::<String>());
+ }
+ if ch.is_whitespace() || !ch.is_ascii() {
+ break;
+ }
+ query.push(ch);
+ }
+ return None;
+ }) else {
+ return Task::ready(Ok(vec![]));
+ };
+
+ let start_offset = end_offset - query.len();
+ let start_anchor = buffer.read(cx).anchor_before(start_offset);
+
+ let candidates = self
+ .users
+ .keys()
+ .map(|user| StringMatchCandidate {
+ id: 0,
+ string: user.clone(),
+ char_bag: user.chars().collect(),
+ })
+ .collect::<Vec<_>>();
+ cx.spawn(|_, cx| async move {
+ let matches = fuzzy::match_strings(
+ &candidates,
+ &query,
+ true,
+ 10,
+ &Default::default(),
+ cx.background_executor().clone(),
+ )
+ .await;
+
+ Ok(matches
+ .into_iter()
+ .map(|mat| Completion {
+ old_range: start_anchor..end_anchor,
+ new_text: mat.string.clone(),
+ label: CodeLabel {
+ filter_range: 1..mat.string.len() + 1,
+ text: format!("@{}", mat.string),
+ runs: Vec::new(),
+ },
+ 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<MessageEditor>,
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,
)
});
@@ -364,6 +364,7 @@ pub struct Editor {
active_diagnostics: Option<ActiveDiagnosticGroup>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
project: Option<Model<Project>>,
+ completion_provider: Option<Box<dyn CompletionProvider>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>,
show_cursor_names: bool,
@@ -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<dyn CompletionProvider>) {
+ 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<Project> {
}
}
+pub trait CompletionProvider {
+ fn completions(
+ &self,
+ buffer: &Model<Buffer>,
+ buffer_position: text::Anchor,
+ cx: &mut ViewContext<Editor>,
+ ) -> Task<Result<Vec<Completion>>>;
+ fn resolve_completions(
+ &self,
+ completion_indices: Vec<usize>,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Task<Result<bool>>;
+}
+
+impl CompletionProvider for Model<Project> {
+ fn completions(
+ &self,
+ buffer: &Model<Buffer>,
+ buffer_position: text::Anchor,
+ cx: &mut ViewContext<Editor>,
+ ) -> Task<Result<Vec<Completion>>> {
+ self.update(cx, |project, cx| {
+ project.completions(&buffer, buffer_position, cx)
+ })
+ }
+
+ fn resolve_completions(
+ &self,
+ completion_indices: Vec<usize>,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Task<Result<bool>> {
+ self.update(cx, |project, cx| {
+ project.resolve_completions(completion_indices, completions, cx)
+ })
+ }
+}
+
fn inlay_hint_settings(
location: Anchor,
snapshot: &MultiBufferSnapshot,
@@ -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));
}
@@ -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<usize>, HighlightId)>,
+ /// Which part of the label participates
pub filter_range: Range<usize>,
}