Autocomplete mentions (#4171)
Release Notes: - Added autocomplete for @-mentions in the chat panel.
This commit is contained in:
commit
0858db9ebb
8 changed files with 393 additions and 242 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",
|
||||||
|
|
|
@ -183,6 +183,7 @@
|
||||||
"context": "Editor && mode == auto_height",
|
"context": "Editor && mode == auto_height",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"ctrl-enter": "editor::Newline",
|
"ctrl-enter": "editor::Newline",
|
||||||
|
"shift-enter": "editor::Newline",
|
||||||
"ctrl-shift-enter": "editor::NewlineBelow"
|
"ctrl-shift-enter": "editor::NewlineBelow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,43 @@ 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_additional_edits_for_completion(
|
||||||
|
&self,
|
||||||
|
_buffer: Model<Buffer>,
|
||||||
|
_completion: Completion,
|
||||||
|
_push_to_history: bool,
|
||||||
|
_cx: &mut ViewContext<Editor>,
|
||||||
|
) -> Task<Result<Option<language::Transaction>>> {
|
||||||
|
Task::ready(Ok(None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MessageEditor {
|
impl MessageEditor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
@ -38,8 +80,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 +193,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(),
|
||||||
|
},
|
||||||
|
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(
|
async fn find_mentions(
|
||||||
this: WeakView<MessageEditor>,
|
this: WeakView<MessageEditor>,
|
||||||
buffer: BufferSnapshot,
|
buffer: BufferSnapshot,
|
||||||
|
|
|
@ -40,7 +40,7 @@ pub(crate) use actions::*;
|
||||||
use aho_corasick::AhoCorasick;
|
use aho_corasick::AhoCorasick;
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use blink_manager::BlinkManager;
|
use blink_manager::BlinkManager;
|
||||||
use client::{Client, Collaborator, ParticipantIndex};
|
use client::{Collaborator, ParticipantIndex};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
|
@ -71,8 +71,7 @@ use language::{
|
||||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
|
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
|
||||||
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
|
CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
|
||||||
Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection,
|
Language, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
|
||||||
SelectionGoal, TransactionId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
|
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
|
||||||
|
@ -88,7 +87,7 @@ use ordered_float::OrderedFloat;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
|
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use rpc::proto::{self, *};
|
use rpc::proto::*;
|
||||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
||||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -365,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,
|
||||||
|
@ -731,85 +731,21 @@ 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 client = project.read(cx).client();
|
let resolve_task = provider.resolve_completions(
|
||||||
let language_registry = project.read(cx).languages().clone();
|
self.matches.iter().map(|m| m.candidate_id).collect(),
|
||||||
|
self.completions.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
let is_remote = project.read(cx).is_remote();
|
return Some(cx.spawn(move |this, mut cx| async move {
|
||||||
let project_id = project.read(cx).remote_id();
|
if let Some(true) = resolve_task.await.log_err() {
|
||||||
|
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attempt_resolve_selected_completion_documentation(
|
fn attempt_resolve_selected_completion_documentation(
|
||||||
|
@ -826,146 +762,16 @@ impl CompletionsMenu {
|
||||||
let Some(project) = project else {
|
let Some(project) = project else {
|
||||||
return;
|
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();
|
|
||||||
|
|
||||||
|
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 {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
Self::resolve_completion_documentation_remote(
|
if let Some(true) = resolve_task.await.log_err() {
|
||||||
project_id,
|
this.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||||
server_id,
|
}
|
||||||
completions.clone(),
|
|
||||||
completion_index,
|
|
||||||
completion,
|
|
||||||
client,
|
|
||||||
language_registry.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
_ = this.update(&mut cx, |_, cx| cx.notify());
|
|
||||||
})
|
})
|
||||||
.detach();
|
.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<RwLock<Box<[Completion]>>>,
|
|
||||||
completion_index: usize,
|
|
||||||
completion: lsp::CompletionItem,
|
|
||||||
client: Arc<Client>,
|
|
||||||
language_registry: Arc<LanguageRegistry>,
|
|
||||||
) {
|
|
||||||
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<lsp::LanguageServer>,
|
|
||||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
|
||||||
completion_index: usize,
|
|
||||||
completion: lsp::CompletionItem,
|
|
||||||
language_registry: Arc<LanguageRegistry>,
|
|
||||||
) {
|
|
||||||
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::<lsp::request::ResolveCompletionItem>(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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visible(&self) -> bool {
|
fn visible(&self) -> bool {
|
||||||
|
@ -1574,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(),
|
||||||
|
@ -1806,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()
|
||||||
}
|
}
|
||||||
|
@ -3252,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -3270,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| {
|
||||||
|
@ -3381,6 +3188,7 @@ impl Editor {
|
||||||
let buffer_handle = completions_menu.buffer;
|
let buffer_handle = completions_menu.buffer;
|
||||||
let completions = completions_menu.completions.read();
|
let completions = completions_menu.completions.read();
|
||||||
let completion = completions.get(mat.candidate_id)?;
|
let completion = completions.get(mat.candidate_id)?;
|
||||||
|
cx.stop_propagation();
|
||||||
|
|
||||||
let snippet;
|
let snippet;
|
||||||
let text;
|
let text;
|
||||||
|
@ -3477,15 +3285,13 @@ impl Editor {
|
||||||
this.refresh_copilot_suggestions(true, cx);
|
this.refresh_copilot_suggestions(true, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = self.project.clone()?;
|
let provider = self.completion_provider.as_ref()?;
|
||||||
let apply_edits = project.update(cx, |project, cx| {
|
let apply_edits = provider.apply_additional_edits_for_completion(
|
||||||
project.apply_additional_edits_for_completion(
|
|
||||||
buffer_handle,
|
buffer_handle,
|
||||||
completion.clone(),
|
completion.clone(),
|
||||||
true,
|
true,
|
||||||
cx,
|
cx,
|
||||||
)
|
);
|
||||||
});
|
|
||||||
Some(cx.foreground_executor().spawn(async move {
|
Some(cx.foreground_executor().spawn(async move {
|
||||||
apply_edits.await?;
|
apply_edits.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -9097,6 +8903,66 @@ 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>>;
|
||||||
|
|
||||||
|
fn apply_additional_edits_for_completion(
|
||||||
|
&self,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
completion: Completion,
|
||||||
|
push_to_history: bool,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) -> Task<Result<Option<language::Transaction>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 apply_additional_edits_for_completion(
|
||||||
|
&self,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
completion: Completion,
|
||||||
|
push_to_history: bool,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) -> Task<Result<Option<language::Transaction>>> {
|
||||||
|
self.update(cx, |project, cx| {
|
||||||
|
project.apply_additional_edits_for_completion(buffer, completion, push_to_history, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn inlay_hint_settings(
|
fn inlay_hint_settings(
|
||||||
location: Anchor,
|
location: Anchor,
|
||||||
snapshot: &MultiBufferSnapshot,
|
snapshot: &MultiBufferSnapshot,
|
||||||
|
|
|
@ -2130,7 +2130,13 @@ impl EditorElement {
|
||||||
if let Some(newest_selection_head) = newest_selection_head {
|
if let Some(newest_selection_head) = newest_selection_head {
|
||||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||||
if editor.context_menu_visible() {
|
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 =
|
context_menu =
|
||||||
editor.render_context_menu(newest_selection_head, &self.style, max_height, cx);
|
editor.render_context_menu(newest_selection_head, &self.style, max_height, cx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -379,8 +379,11 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct CodeLabel {
|
pub struct CodeLabel {
|
||||||
|
/// The text to display.
|
||||||
pub text: String,
|
pub text: String,
|
||||||
|
/// Syntax highlighting runs.
|
||||||
pub runs: Vec<(Range<usize>, HighlightId)>,
|
pub runs: Vec<(Range<usize>, HighlightId)>,
|
||||||
|
/// The portion of the text that should be used in fuzzy filtering.
|
||||||
pub filter_range: Range<usize>,
|
pub filter_range: Range<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,16 +34,16 @@ use gpui::{
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
||||||
point_to_lsp,
|
markdown, point_to_lsp,
|
||||||
proto::{
|
proto::{
|
||||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||||
serialize_anchor, serialize_version, split_operations,
|
serialize_anchor, serialize_version, split_operations,
|
||||||
},
|
},
|
||||||
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
|
range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
|
||||||
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
|
CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
|
||||||
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
|
Documentation, Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName,
|
||||||
LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
|
LocalFile, LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer,
|
||||||
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||||
};
|
};
|
||||||
use log::error;
|
use log::error;
|
||||||
use lsp::{
|
use lsp::{
|
||||||
|
@ -52,7 +52,7 @@ use lsp::{
|
||||||
};
|
};
|
||||||
use lsp_command::*;
|
use lsp_command::*;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::{Mutex, RwLock};
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use prettier_support::{DefaultPrettier, PrettierInstance};
|
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||||
use project_settings::{LspSettings, ProjectSettings};
|
use project_settings::{LspSettings, ProjectSettings};
|
||||||
|
@ -4828,6 +4828,170 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resolve_completions(
|
||||||
|
&self,
|
||||||
|
completion_indices: Vec<usize>,
|
||||||
|
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
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<lsp::LanguageServer>,
|
||||||
|
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||||
|
completion_index: usize,
|
||||||
|
completion: lsp::CompletionItem,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
) {
|
||||||
|
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::<lsp::request::ResolveCompletionItem>(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<RwLock<Box<[Completion]>>>,
|
||||||
|
completion_index: usize,
|
||||||
|
completion: lsp::CompletionItem,
|
||||||
|
client: Arc<Client>,
|
||||||
|
language_registry: Arc<LanguageRegistry>,
|
||||||
|
) {
|
||||||
|
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(
|
pub fn apply_additional_edits_for_completion(
|
||||||
&self,
|
&self,
|
||||||
buffer_handle: Model<Buffer>,
|
buffer_handle: Model<Buffer>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue