Start work on autocomplete for chat mentions

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-19 13:12:33 -08:00
parent 1ceccdf03b
commit 139986d080
6 changed files with 165 additions and 22 deletions

1
Cargo.lock generated
View file

@ -1548,6 +1548,7 @@ dependencies = [
"log",
"menu",
"notifications",
"parking_lot 0.11.2",
"picker",
"postage",
"pretty_assertions",

View file

@ -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

View file

@ -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,
)
});

View file

@ -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,

View file

@ -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));
}

View file

@ -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>,
}