Use LspCommand to handle completions

This commit is contained in:
Antonio Scandurra 2023-04-11 14:52:07 +02:00
parent 9e6d865882
commit ac532cb6fa
2 changed files with 212 additions and 224 deletions

View File

@ -4,11 +4,13 @@ use crate::{
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use client::proto::{self, PeerId};
use fs::LineEnding;
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{
point_from_lsp, point_to_lsp,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
range_from_lsp, Anchor, Bias, Buffer, CachedLspAdapter, PointUtf16, ToPointUtf16,
range_from_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, Completion, PointUtf16,
ToOffset, ToPointUtf16, Unclipped,
};
use lsp::{DocumentHighlightKind, LanguageServer, ServerCapabilities};
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
@ -91,6 +93,10 @@ pub(crate) struct GetHover {
pub position: PointUtf16,
}
pub(crate) struct GetCompletions {
pub position: PointUtf16,
}
#[async_trait(?Send)]
impl LspCommand for PrepareRename {
type Response = Option<Range<Anchor>>;
@ -1199,3 +1205,204 @@ impl LspCommand for GetHover {
message.buffer_id
}
}
#[async_trait(?Send)]
impl LspCommand for GetCompletions {
type Response = Vec<Completion>;
type LspRequest = lsp::request::Completion;
type ProtoRequest = proto::GetCompletions;
fn to_lsp(&self, path: &Path, _: &AppContext) -> lsp::CompletionParams {
lsp::CompletionParams {
text_document_position: lsp::TextDocumentPositionParams::new(
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path).unwrap()),
point_to_lsp(self.position),
),
context: Default::default(),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
}
}
async fn response_from_lsp(
self,
completions: Option<lsp::CompletionResponse>,
_: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
cx: AsyncAppContext,
) -> Result<Vec<Completion>> {
let completions = if let Some(completions) = completions {
match completions {
lsp::CompletionResponse::Array(completions) => completions,
lsp::CompletionResponse::List(list) => list.items,
}
} else {
Default::default()
};
let completions = buffer.read_with(&cx, |buffer, _| {
let language = buffer.language().cloned();
let snapshot = buffer.snapshot();
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
let mut range_for_token = None;
completions
.into_iter()
.filter_map(move |mut lsp_completion| {
// For now, we can only handle additional edits if they are returned
// when resolving the completion, not if they are present initially.
if lsp_completion
.additional_text_edits
.as_ref()
.map_or(false, |edits| !edits.is_empty())
{
return None;
}
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref() {
// If the language server provides a range to overwrite, then
// check that the range is valid.
Some(lsp::CompletionTextEdit::Edit(edit)) => {
let range = range_from_lsp(edit.range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range");
return None;
}
(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
edit.new_text.clone(),
)
}
// If the language server does not provide a range, then infer
// the range based on the syntax tree.
None => {
if self.position != clipped_position {
log::info!("completion out of expected range");
return None;
}
let Range { start, end } = range_for_token
.get_or_insert_with(|| {
let offset = self.position.to_offset(&snapshot);
let (range, kind) = snapshot.surrounding_word(offset);
if kind == Some(CharKind::Word) {
range
} else {
offset..offset
}
})
.clone();
let text = lsp_completion
.insert_text
.as_ref()
.unwrap_or(&lsp_completion.label)
.clone();
(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
text,
)
}
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
log::info!("unsupported insert/replace completion");
return None;
}
};
let language = language.clone();
LineEnding::normalize(&mut new_text);
Some(async move {
let mut label = None;
if let Some(language) = language {
language.process_completion(&mut lsp_completion).await;
label = language.label_for_completion(&lsp_completion).await;
}
Completion {
old_range,
new_text,
label: label.unwrap_or_else(|| {
language::CodeLabel::plain(
lsp_completion.label.clone(),
lsp_completion.filter_text.as_deref(),
)
}),
lsp_completion,
}
})
})
});
Ok(futures::future::join_all(completions).await)
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions {
let anchor = buffer.anchor_after(self.position);
proto::GetCompletions {
project_id,
buffer_id: buffer.remote_id(),
position: Some(language::proto::serialize_anchor(&anchor)),
version: serialize_version(&buffer.version()),
}
}
async fn from_proto(
message: proto::GetCompletions,
project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
mut cx: AsyncAppContext,
) -> Result<Self> {
let version = deserialize_version(&message.version);
buffer
.update(&mut cx, |buffer, _| buffer.wait_for_version(version))
.await?;
let position = message
.position
.and_then(language::proto::deserialize_anchor)
.map(|p| {
buffer.read_with(&cx, |buffer, _| {
buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left)
})
})
.ok_or_else(|| anyhow!("invalid position"))?;
Ok(Self { position })
}
fn response_to_proto(
completions: Vec<Completion>,
_: &mut Project,
_: PeerId,
buffer_version: &clock::Global,
_: &mut AppContext,
) -> proto::GetCompletionsResponse {
proto::GetCompletionsResponse {
completions: completions
.iter()
.map(language::proto::serialize_completion)
.collect(),
version: serialize_version(&buffer_version),
}
}
async fn response_from_proto(
self,
message: proto::GetCompletionsResponse,
_: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
mut cx: AsyncAppContext,
) -> Result<Vec<Completion>> {
buffer
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&message.version))
})
.await?;
let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned());
let completions = message.completions.into_iter().map(|completion| {
language::proto::deserialize_completion(completion, language.clone())
});
futures::future::try_join_all(completions).await
}
fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 {
message.buffer_id
}
}

View File

@ -410,7 +410,7 @@ impl Project {
client.add_model_request_handler(Self::handle_synchronize_buffers);
client.add_model_request_handler(Self::handle_format_buffers);
client.add_model_request_handler(Self::handle_get_code_actions);
client.add_model_request_handler(Self::handle_get_completions);
client.add_model_request_handler(Self::handle_lsp_command::<GetCompletions>);
client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
client.add_model_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
@ -3596,188 +3596,12 @@ impl Project {
pub fn completions<T: ToPointUtf16>(
&self,
source_buffer_handle: &ModelHandle<Buffer>,
buffer: &ModelHandle<Buffer>,
position: T,
cx: &mut ModelContext<Self>,
) -> Task<Result<Vec<Completion>>> {
let source_buffer_handle = source_buffer_handle.clone();
let source_buffer = source_buffer_handle.read(cx);
let buffer_id = source_buffer.remote_id();
let language = source_buffer.language().cloned();
let worktree;
let buffer_abs_path;
if let Some(file) = File::from_dyn(source_buffer.file()) {
worktree = file.worktree.clone();
buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
} else {
return Task::ready(Ok(Default::default()));
};
let position = Unclipped(position.to_point_utf16(source_buffer));
let anchor = source_buffer.anchor_after(position);
if worktree.read(cx).as_local().is_some() {
let buffer_abs_path = buffer_abs_path.unwrap();
let lang_server =
if let Some((_, server)) = self.language_server_for_buffer(source_buffer, cx) {
server.clone()
} else {
return Task::ready(Ok(Default::default()));
};
cx.spawn(|_, cx| async move {
let completions = lang_server
.request::<lsp::request::Completion>(lsp::CompletionParams {
text_document_position: lsp::TextDocumentPositionParams::new(
lsp::TextDocumentIdentifier::new(
lsp::Url::from_file_path(buffer_abs_path).unwrap(),
),
point_to_lsp(position.0),
),
context: Default::default(),
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.await
.context("lsp completion request failed")?;
let completions = if let Some(completions) = completions {
match completions {
lsp::CompletionResponse::Array(completions) => completions,
lsp::CompletionResponse::List(list) => list.items,
}
} else {
Default::default()
};
let completions = source_buffer_handle.read_with(&cx, |this, _| {
let snapshot = this.snapshot();
let clipped_position = this.clip_point_utf16(position, Bias::Left);
let mut range_for_token = None;
completions
.into_iter()
.filter_map(move |mut lsp_completion| {
// For now, we can only handle additional edits if they are returned
// when resolving the completion, not if they are present initially.
if lsp_completion
.additional_text_edits
.as_ref()
.map_or(false, |edits| !edits.is_empty())
{
return None;
}
let (old_range, mut new_text) = match lsp_completion.text_edit.as_ref()
{
// If the language server provides a range to overwrite, then
// check that the range is valid.
Some(lsp::CompletionTextEdit::Edit(edit)) => {
let range = range_from_lsp(edit.range);
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
if start != range.start.0 || end != range.end.0 {
log::info!("completion out of expected range");
return None;
}
(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
edit.new_text.clone(),
)
}
// If the language server does not provide a range, then infer
// the range based on the syntax tree.
None => {
if position.0 != clipped_position {
log::info!("completion out of expected range");
return None;
}
let Range { start, end } = range_for_token
.get_or_insert_with(|| {
let offset = position.to_offset(&snapshot);
let (range, kind) = snapshot.surrounding_word(offset);
if kind == Some(CharKind::Word) {
range
} else {
offset..offset
}
})
.clone();
let text = lsp_completion
.insert_text
.as_ref()
.unwrap_or(&lsp_completion.label)
.clone();
(
snapshot.anchor_before(start)..snapshot.anchor_after(end),
text,
)
}
Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
log::info!("unsupported insert/replace completion");
return None;
}
};
LineEnding::normalize(&mut new_text);
let language = language.clone();
Some(async move {
let mut label = None;
if let Some(language) = language {
language.process_completion(&mut lsp_completion).await;
label = language.label_for_completion(&lsp_completion).await;
}
Completion {
old_range,
new_text,
label: label.unwrap_or_else(|| {
CodeLabel::plain(
lsp_completion.label.clone(),
lsp_completion.filter_text.as_deref(),
)
}),
lsp_completion,
}
})
})
});
Ok(futures::future::join_all(completions).await)
})
} else if let Some(project_id) = self.remote_id() {
let rpc = self.client.clone();
let message = proto::GetCompletions {
project_id,
buffer_id,
position: Some(language::proto::serialize_anchor(&anchor)),
version: serialize_version(&source_buffer.version()),
};
cx.spawn_weak(|this, mut cx| async move {
let response = rpc.request(message).await?;
if this
.upgrade(&cx)
.ok_or_else(|| anyhow!("project was dropped"))?
.read_with(&cx, |this, _| this.is_read_only())
{
return Err(anyhow!(
"failed to get completions: project was disconnected"
));
} else {
source_buffer_handle
.update(&mut cx, |buffer, _| {
buffer.wait_for_version(deserialize_version(&response.version))
})
.await?;
let completions = response.completions.into_iter().map(|completion| {
language::proto::deserialize_completion(completion, language.clone())
});
futures::future::try_join_all(completions).await
}
})
} else {
Task::ready(Ok(Default::default()))
}
let position = position.to_point_utf16(buffer.read(cx));
self.request_lsp(buffer.clone(), GetCompletions { position }, cx)
}
pub fn apply_additional_edits_for_completion(
@ -5632,49 +5456,6 @@ impl Project {
})
}
async fn handle_get_completions(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::GetCompletions>,
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<proto::GetCompletionsResponse> {
let buffer = this.read_with(&cx, |this, cx| {
this.opened_buffers
.get(&envelope.payload.buffer_id)
.and_then(|buffer| buffer.upgrade(cx))
.ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))
})?;
let version = deserialize_version(&envelope.payload.version);
buffer
.update(&mut cx, |buffer, _| buffer.wait_for_version(version))
.await?;
let version = buffer.read_with(&cx, |buffer, _| buffer.version());
let position = envelope
.payload
.position
.and_then(language::proto::deserialize_anchor)
.map(|p| {
buffer.read_with(&cx, |buffer, _| {
buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left)
})
})
.ok_or_else(|| anyhow!("invalid position"))?;
let completions = this
.update(&mut cx, |this, cx| this.completions(&buffer, position, cx))
.await?;
Ok(proto::GetCompletionsResponse {
completions: completions
.iter()
.map(language::proto::serialize_completion)
.collect(),
version: serialize_version(&version),
})
}
async fn handle_apply_additional_edits_for_completion(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::ApplyCompletionAdditionalEdits>,