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:
parent
1ceccdf03b
commit
139986d080
6 changed files with 165 additions and 22 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue