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",
|
"log",
|
||||||
"menu",
|
"menu",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
"picker",
|
"picker",
|
||||||
"postage",
|
"postage",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
|
|
|
@ -60,6 +60,7 @@ anyhow.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
serde.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 channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
|
||||||
use client::UserId;
|
use client::UserId;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
|
use editor::{AnchorRangeExt, CompletionProvider, Editor, EditorElement, EditorStyle};
|
||||||
|
use fuzzy::StringMatchCandidate;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
|
||||||
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
|
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 lazy_static::lazy_static;
|
||||||
|
use parking_lot::RwLock;
|
||||||
use project::search::SearchQuery;
|
use project::search::SearchQuery;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
|
||||||
|
@ -31,6 +36,33 @@ pub struct MessageEditor {
|
||||||
channel_id: Option<ChannelId>,
|
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 {
|
impl MessageEditor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
@ -38,8 +70,10 @@ impl MessageEditor {
|
||||||
editor: View<Editor>,
|
editor: View<Editor>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let this = cx.view().downgrade();
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
|
||||||
|
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
|
||||||
});
|
});
|
||||||
|
|
||||||
let buffer = editor
|
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(
|
async fn find_mentions(
|
||||||
this: WeakView<MessageEditor>,
|
this: WeakView<MessageEditor>,
|
||||||
buffer: BufferSnapshot,
|
buffer: BufferSnapshot,
|
||||||
|
@ -227,6 +326,7 @@ impl Render for MessageEditor {
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
.h(px(500.))
|
||||||
.px_2()
|
.px_2()
|
||||||
.py_1()
|
.py_1()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
@ -260,7 +360,7 @@ mod tests {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
language_registry,
|
language_registry,
|
||||||
ChannelStore::global(cx),
|
ChannelStore::global(cx),
|
||||||
cx.new_view(|cx| Editor::auto_height(4, cx)),
|
cx.new_view(|cx| Editor::auto_height(25, cx)),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -364,6 +364,7 @@ pub struct Editor {
|
||||||
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
||||||
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
|
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
|
||||||
project: Option<Model<Project>>,
|
project: Option<Model<Project>>,
|
||||||
|
completion_provider: Option<Box<dyn CompletionProvider>>,
|
||||||
collaboration_hub: Option<Box<dyn CollaborationHub>>,
|
collaboration_hub: Option<Box<dyn CollaborationHub>>,
|
||||||
blink_manager: Model<BlinkManager>,
|
blink_manager: Model<BlinkManager>,
|
||||||
show_cursor_names: bool,
|
show_cursor_names: bool,
|
||||||
|
@ -730,17 +731,15 @@ impl CompletionsMenu {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(project) = editor.project.clone() else {
|
let Some(provider) = editor.completion_provider.as_ref() else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
let resolve_task = project.update(cx, |project, cx| {
|
let resolve_task = provider.resolve_completions(
|
||||||
project.resolve_completions(
|
self.matches.iter().map(|m| m.candidate_id).collect(),
|
||||||
self.matches.iter().map(|m| m.candidate_id).collect(),
|
self.completions.clone(),
|
||||||
self.completions.clone(),
|
cx,
|
||||||
cx,
|
);
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
return Some(cx.spawn(move |this, mut cx| async move {
|
return Some(cx.spawn(move |this, mut cx| async move {
|
||||||
if let Some(true) = resolve_task.await.log_err() {
|
if let Some(true) = resolve_task.await.log_err() {
|
||||||
|
@ -1381,6 +1380,7 @@ impl Editor {
|
||||||
ime_transaction: Default::default(),
|
ime_transaction: Default::default(),
|
||||||
active_diagnostics: None,
|
active_diagnostics: None,
|
||||||
soft_wrap_mode_override,
|
soft_wrap_mode_override,
|
||||||
|
completion_provider: project.clone().map(|project| Box::new(project) as _),
|
||||||
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
|
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
|
||||||
project,
|
project,
|
||||||
blink_manager: blink_manager.clone(),
|
blink_manager: blink_manager.clone(),
|
||||||
|
@ -1613,6 +1613,10 @@ impl Editor {
|
||||||
self.collaboration_hub = Some(hub);
|
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> {
|
pub fn placeholder_text(&self) -> Option<&str> {
|
||||||
self.placeholder_text.as_deref()
|
self.placeholder_text.as_deref()
|
||||||
}
|
}
|
||||||
|
@ -3059,9 +3063,7 @@ impl Editor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let project = if let Some(project) = self.project.clone() {
|
let Some(provider) = self.completion_provider.as_ref() else {
|
||||||
project
|
|
||||||
} else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3077,9 +3079,7 @@ impl Editor {
|
||||||
};
|
};
|
||||||
|
|
||||||
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone());
|
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone());
|
||||||
let completions = project.update(cx, |project, cx| {
|
let completions = provider.completions(&buffer, buffer_position, cx);
|
||||||
project.completions(&buffer, buffer_position, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let id = post_inc(&mut self.next_completion_id);
|
let id = post_inc(&mut self.next_completion_id);
|
||||||
let task = cx.spawn(|this, mut cx| {
|
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(
|
fn inlay_hint_settings(
|
||||||
location: Anchor,
|
location: Anchor,
|
||||||
snapshot: &MultiBufferSnapshot,
|
snapshot: &MultiBufferSnapshot,
|
||||||
|
|
|
@ -1177,9 +1177,9 @@ impl EditorElement {
|
||||||
list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
|
list_origin.x = (cx.viewport_size().width - list_width).max(Pixels::ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
if list_origin.y + list_height > text_bounds.lower_right().y {
|
// if list_origin.y + list_height > text_bounds.lower_right().y {
|
||||||
list_origin.y -= layout.position_map.line_height + list_height;
|
// list_origin.y -= layout.position_map.line_height + list_height;
|
||||||
}
|
// }
|
||||||
|
|
||||||
cx.break_content_mask(|cx| context_menu.draw(list_origin, available_space, cx));
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct CodeLabel {
|
pub struct CodeLabel {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
/// Determines the syntax highlighting for the label
|
||||||
pub runs: Vec<(Range<usize>, HighlightId)>,
|
pub runs: Vec<(Range<usize>, HighlightId)>,
|
||||||
|
/// Which part of the label participates
|
||||||
pub filter_range: Range<usize>,
|
pub filter_range: Range<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue