
Instead of holding a connection for potentially long LSP queries (e.g. rust-analyzer might take minutes to look up a definition), disconnect right after sending the initial request and handle the follow-up responses later. As a bonus, this allows to cancel previously sent request on the local Collab clients' side due to this, as instead of holding and serving the old connection, local clients now can stop previous requests, if needed. Current PR does not convert all LSP requests to the new paradigm, but the problematic ones, deprecating `MultiLspQuery` and moving all its requests to the new paradigm. Release Notes: - Improved resource usage when querying LSP over Collab --------- Co-authored-by: David Kleingeld <git@davidsk.dev> Co-authored-by: Mikayla Maki <mikayla@zed.dev> Co-authored-by: David Kleingeld <davidsk@zed.dev>
4594 lines
161 KiB
Rust
4594 lines
161 KiB
Rust
mod signature_help;
|
|
|
|
use crate::{
|
|
CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentColor,
|
|
DocumentHighlight, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint,
|
|
InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
|
|
LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse,
|
|
ProjectTransaction, PulledDiagnostics, ResolveState,
|
|
lsp_store::{LocalLspStore, LspStore},
|
|
};
|
|
use anyhow::{Context as _, Result};
|
|
use async_trait::async_trait;
|
|
use client::proto::{self, PeerId};
|
|
use clock::Global;
|
|
use collections::{HashMap, HashSet};
|
|
use futures::future;
|
|
use gpui::{App, AsyncApp, Entity, Task};
|
|
use language::{
|
|
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, OffsetRangeExt, PointUtf16,
|
|
ToOffset, ToPointUtf16, Transaction, Unclipped,
|
|
language_settings::{InlayHintKind, LanguageSettings, language_settings},
|
|
point_from_lsp, point_to_lsp,
|
|
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
|
range_from_lsp, range_to_lsp,
|
|
};
|
|
use lsp::{
|
|
AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription,
|
|
CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind,
|
|
DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
|
|
OneOf, RenameOptions, ServerCapabilities,
|
|
};
|
|
use serde_json::Value;
|
|
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
|
use std::{
|
|
cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc,
|
|
};
|
|
use text::{BufferId, LineEnding};
|
|
use util::{ResultExt as _, debug_panic};
|
|
|
|
pub use signature_help::SignatureHelp;
|
|
|
|
pub fn lsp_formatting_options(settings: &LanguageSettings) -> lsp::FormattingOptions {
|
|
lsp::FormattingOptions {
|
|
tab_size: settings.tab_size.into(),
|
|
insert_spaces: !settings.hard_tabs,
|
|
trim_trailing_whitespace: Some(settings.remove_trailing_whitespace_on_save),
|
|
trim_final_newlines: Some(settings.ensure_final_newline_on_save),
|
|
insert_final_newline: Some(settings.ensure_final_newline_on_save),
|
|
..lsp::FormattingOptions::default()
|
|
}
|
|
}
|
|
|
|
pub fn file_path_to_lsp_url(path: &Path) -> Result<lsp::Url> {
|
|
match lsp::Url::from_file_path(path) {
|
|
Ok(url) => Ok(url),
|
|
Err(()) => anyhow::bail!("Invalid file path provided to LSP request: {path:?}"),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn make_text_document_identifier(path: &Path) -> Result<lsp::TextDocumentIdentifier> {
|
|
Ok(lsp::TextDocumentIdentifier {
|
|
uri: file_path_to_lsp_url(path)?,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn make_lsp_text_document_position(
|
|
path: &Path,
|
|
position: PointUtf16,
|
|
) -> Result<lsp::TextDocumentPositionParams> {
|
|
Ok(lsp::TextDocumentPositionParams {
|
|
text_document: make_text_document_identifier(path)?,
|
|
position: point_to_lsp(position),
|
|
})
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
pub trait LspCommand: 'static + Sized + Send + std::fmt::Debug {
|
|
type Response: 'static + Default + Send + std::fmt::Debug;
|
|
type LspRequest: 'static + Send + lsp::request::Request;
|
|
type ProtoRequest: 'static + Send + proto::RequestMessage;
|
|
|
|
fn display_name(&self) -> &str;
|
|
|
|
fn status(&self) -> Option<String> {
|
|
None
|
|
}
|
|
|
|
fn to_lsp_params_or_response(
|
|
&self,
|
|
path: &Path,
|
|
buffer: &Buffer,
|
|
language_server: &Arc<LanguageServer>,
|
|
cx: &App,
|
|
) -> Result<
|
|
LspParamsOrResponse<<Self::LspRequest as lsp::request::Request>::Params, Self::Response>,
|
|
> {
|
|
if self.check_capabilities(language_server.adapter_server_capabilities()) {
|
|
Ok(LspParamsOrResponse::Params(self.to_lsp(
|
|
path,
|
|
buffer,
|
|
language_server,
|
|
cx,
|
|
)?))
|
|
} else {
|
|
Ok(LspParamsOrResponse::Response(Default::default()))
|
|
}
|
|
}
|
|
|
|
/// When false, `to_lsp_params_or_response` default implementation will return the default response.
|
|
fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool;
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
buffer: &Buffer,
|
|
language_server: &Arc<LanguageServer>,
|
|
cx: &App,
|
|
) -> Result<<Self::LspRequest as lsp::request::Request>::Params>;
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: <Self::LspRequest as lsp::request::Request>::Result,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Self::Response>;
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest;
|
|
|
|
async fn from_proto(
|
|
message: Self::ProtoRequest,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
cx: AsyncApp,
|
|
) -> Result<Self>;
|
|
|
|
fn response_to_proto(
|
|
response: Self::Response,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
buffer_version: &clock::Global,
|
|
cx: &mut App,
|
|
) -> <Self::ProtoRequest as proto::RequestMessage>::Response;
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: <Self::ProtoRequest as proto::RequestMessage>::Response,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
cx: AsyncApp,
|
|
) -> Result<Self::Response>;
|
|
|
|
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId>;
|
|
}
|
|
|
|
pub enum LspParamsOrResponse<P, R> {
|
|
Params(P),
|
|
Response(R),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct PrepareRename {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct PerformRename {
|
|
pub position: PointUtf16,
|
|
pub new_name: String,
|
|
pub push_to_history: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct GetDefinitions {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub(crate) struct GetDeclarations {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub(crate) struct GetTypeDefinitions {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub(crate) struct GetImplementations {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub(crate) struct GetReferences {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct GetDocumentHighlights {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub(crate) struct GetDocumentSymbols;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct GetSignatureHelp {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct GetHover {
|
|
pub position: PointUtf16,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct GetCompletions {
|
|
pub position: PointUtf16,
|
|
pub context: CompletionContext,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct GetCodeActions {
|
|
pub range: Range<Anchor>,
|
|
pub kinds: Option<Vec<lsp::CodeActionKind>>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct OnTypeFormatting {
|
|
pub position: PointUtf16,
|
|
pub trigger: String,
|
|
pub options: lsp::FormattingOptions,
|
|
pub push_to_history: bool,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct InlayHints {
|
|
pub range: Range<Anchor>,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub(crate) struct GetCodeLens;
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub(crate) struct GetDocumentColor;
|
|
|
|
impl GetCodeLens {
|
|
pub(crate) fn can_resolve_lens(capabilities: &ServerCapabilities) -> bool {
|
|
capabilities
|
|
.code_lens_provider
|
|
.as_ref()
|
|
.and_then(|code_lens_options| code_lens_options.resolve_provider)
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) struct LinkedEditingRange {
|
|
pub position: Anchor,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct GetDocumentDiagnostics {
|
|
pub previous_result_id: Option<String>,
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for PrepareRename {
|
|
type Response = PrepareRenameResponse;
|
|
type LspRequest = lsp::request::PrepareRenameRequest;
|
|
type ProtoRequest = proto::PrepareRename;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Prepare rename"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.rename_provider
|
|
.is_some_and(|capability| match capability {
|
|
OneOf::Left(enabled) => enabled,
|
|
OneOf::Right(options) => options.prepare_provider.unwrap_or(false),
|
|
})
|
|
}
|
|
|
|
fn to_lsp_params_or_response(
|
|
&self,
|
|
path: &Path,
|
|
buffer: &Buffer,
|
|
language_server: &Arc<LanguageServer>,
|
|
cx: &App,
|
|
) -> Result<LspParamsOrResponse<lsp::TextDocumentPositionParams, PrepareRenameResponse>> {
|
|
let rename_provider = language_server
|
|
.adapter_server_capabilities()
|
|
.server_capabilities
|
|
.rename_provider;
|
|
match rename_provider {
|
|
Some(lsp::OneOf::Right(RenameOptions {
|
|
prepare_provider: Some(true),
|
|
..
|
|
})) => Ok(LspParamsOrResponse::Params(self.to_lsp(
|
|
path,
|
|
buffer,
|
|
language_server,
|
|
cx,
|
|
)?)),
|
|
Some(lsp::OneOf::Right(_)) => Ok(LspParamsOrResponse::Response(
|
|
PrepareRenameResponse::OnlyUnpreparedRenameSupported,
|
|
)),
|
|
Some(lsp::OneOf::Left(true)) => Ok(LspParamsOrResponse::Response(
|
|
PrepareRenameResponse::OnlyUnpreparedRenameSupported,
|
|
)),
|
|
_ => anyhow::bail!("Rename not supported"),
|
|
}
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::TextDocumentPositionParams> {
|
|
make_lsp_text_document_position(path, self.position)
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::PrepareRenameResponse>,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
_: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<PrepareRenameResponse> {
|
|
buffer.read_with(&cx, |buffer, _| match message {
|
|
Some(lsp::PrepareRenameResponse::Range(range))
|
|
| Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }) => {
|
|
let Range { start, end } = range_from_lsp(range);
|
|
if buffer.clip_point_utf16(start, Bias::Left) == start.0
|
|
&& buffer.clip_point_utf16(end, Bias::Left) == end.0
|
|
{
|
|
Ok(PrepareRenameResponse::Success(
|
|
buffer.anchor_after(start)..buffer.anchor_before(end),
|
|
))
|
|
} else {
|
|
Ok(PrepareRenameResponse::InvalidPosition)
|
|
}
|
|
}
|
|
Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => {
|
|
let snapshot = buffer.snapshot();
|
|
let (range, _) = snapshot.surrounding_word(self.position, false);
|
|
let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
|
|
Ok(PrepareRenameResponse::Success(range))
|
|
}
|
|
None => Ok(PrepareRenameResponse::InvalidPosition),
|
|
})?
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareRename {
|
|
proto::PrepareRename {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::PrepareRename,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: PrepareRenameResponse,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
buffer_version: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::PrepareRenameResponse {
|
|
match response {
|
|
PrepareRenameResponse::Success(range) => proto::PrepareRenameResponse {
|
|
can_rename: true,
|
|
only_unprepared_rename_supported: false,
|
|
start: Some(language::proto::serialize_anchor(&range.start)),
|
|
end: Some(language::proto::serialize_anchor(&range.end)),
|
|
version: serialize_version(buffer_version),
|
|
},
|
|
PrepareRenameResponse::OnlyUnpreparedRenameSupported => proto::PrepareRenameResponse {
|
|
can_rename: false,
|
|
only_unprepared_rename_supported: true,
|
|
start: None,
|
|
end: None,
|
|
version: vec![],
|
|
},
|
|
PrepareRenameResponse::InvalidPosition => proto::PrepareRenameResponse {
|
|
can_rename: false,
|
|
only_unprepared_rename_supported: false,
|
|
start: None,
|
|
end: None,
|
|
version: vec![],
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::PrepareRenameResponse,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<PrepareRenameResponse> {
|
|
if message.can_rename {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
if let (Some(start), Some(end)) = (
|
|
message.start.and_then(deserialize_anchor),
|
|
message.end.and_then(deserialize_anchor),
|
|
) {
|
|
Ok(PrepareRenameResponse::Success(start..end))
|
|
} else {
|
|
anyhow::bail!(
|
|
"Missing start or end position in remote project PrepareRenameResponse"
|
|
);
|
|
}
|
|
} else if message.only_unprepared_rename_supported {
|
|
Ok(PrepareRenameResponse::OnlyUnpreparedRenameSupported)
|
|
} else {
|
|
Ok(PrepareRenameResponse::InvalidPosition)
|
|
}
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::PrepareRename) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for PerformRename {
|
|
type Response = ProjectTransaction;
|
|
type LspRequest = lsp::request::Rename;
|
|
type ProtoRequest = proto::PerformRename;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Rename"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.rename_provider
|
|
.is_some_and(|capability| match capability {
|
|
OneOf::Left(enabled) => enabled,
|
|
OneOf::Right(_options) => true,
|
|
})
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::RenameParams> {
|
|
Ok(lsp::RenameParams {
|
|
text_document_position: make_lsp_text_document_position(path, self.position)?,
|
|
new_name: self.new_name.clone(),
|
|
work_done_progress_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::WorkspaceEdit>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
mut cx: AsyncApp,
|
|
) -> Result<ProjectTransaction> {
|
|
if let Some(edit) = message {
|
|
let (_, lsp_server) =
|
|
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
|
LocalLspStore::deserialize_workspace_edit(
|
|
lsp_store,
|
|
edit,
|
|
self.push_to_history,
|
|
lsp_server,
|
|
&mut cx,
|
|
)
|
|
.await
|
|
} else {
|
|
Ok(ProjectTransaction::default())
|
|
}
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename {
|
|
proto::PerformRename {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
new_name: self.new_name.clone(),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::PerformRename,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
new_name: message.new_name,
|
|
push_to_history: false,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: ProjectTransaction,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
_: &clock::Global,
|
|
cx: &mut App,
|
|
) -> proto::PerformRenameResponse {
|
|
let transaction = lsp_store.buffer_store().update(cx, |buffer_store, cx| {
|
|
buffer_store.serialize_project_transaction_for_peer(response, peer_id, cx)
|
|
});
|
|
proto::PerformRenameResponse {
|
|
transaction: Some(transaction),
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::PerformRenameResponse,
|
|
lsp_store: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<ProjectTransaction> {
|
|
let message = message.transaction.context("missing transaction")?;
|
|
lsp_store
|
|
.update(&mut cx, |lsp_store, cx| {
|
|
lsp_store.buffer_store().update(cx, |buffer_store, cx| {
|
|
buffer_store.deserialize_project_transaction(message, self.push_to_history, cx)
|
|
})
|
|
})?
|
|
.await
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::PerformRename) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetDefinitions {
|
|
type Response = Vec<LocationLink>;
|
|
type LspRequest = lsp::request::GotoDefinition;
|
|
type ProtoRequest = proto::GetDefinition;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get definition"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.definition_provider
|
|
.is_some_and(|capability| match capability {
|
|
OneOf::Left(supported) => supported,
|
|
OneOf::Right(_options) => true,
|
|
})
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::GotoDefinitionParams> {
|
|
Ok(lsp::GotoDefinitionParams {
|
|
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::GotoDefinitionResponse>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition {
|
|
proto::GetDefinition {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetDefinition,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<LocationLink>,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
_: &clock::Global,
|
|
cx: &mut App,
|
|
) -> proto::GetDefinitionResponse {
|
|
let links = location_links_to_proto(response, lsp_store, peer_id, cx);
|
|
proto::GetDefinitionResponse { links }
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetDefinitionResponse,
|
|
lsp_store: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
location_links_from_proto(message.links, lsp_store, cx).await
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetDefinition) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetDeclarations {
|
|
type Response = Vec<LocationLink>;
|
|
type LspRequest = lsp::request::GotoDeclaration;
|
|
type ProtoRequest = proto::GetDeclaration;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get declaration"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.declaration_provider
|
|
.is_some_and(|capability| match capability {
|
|
lsp::DeclarationCapability::Simple(supported) => supported,
|
|
lsp::DeclarationCapability::RegistrationOptions(..) => true,
|
|
lsp::DeclarationCapability::Options(..) => true,
|
|
})
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::GotoDeclarationParams> {
|
|
Ok(lsp::GotoDeclarationParams {
|
|
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::GotoDeclarationResponse>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDeclaration {
|
|
proto::GetDeclaration {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetDeclaration,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<LocationLink>,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
_: &clock::Global,
|
|
cx: &mut App,
|
|
) -> proto::GetDeclarationResponse {
|
|
let links = location_links_to_proto(response, lsp_store, peer_id, cx);
|
|
proto::GetDeclarationResponse { links }
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetDeclarationResponse,
|
|
lsp_store: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
location_links_from_proto(message.links, lsp_store, cx).await
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetDeclaration) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetImplementations {
|
|
type Response = Vec<LocationLink>;
|
|
type LspRequest = lsp::request::GotoImplementation;
|
|
type ProtoRequest = proto::GetImplementation;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get implementation"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.implementation_provider
|
|
.is_some_and(|capability| match capability {
|
|
lsp::ImplementationProviderCapability::Simple(enabled) => enabled,
|
|
lsp::ImplementationProviderCapability::Options(_options) => true,
|
|
})
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::GotoImplementationParams> {
|
|
Ok(lsp::GotoImplementationParams {
|
|
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::GotoImplementationResponse>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetImplementation {
|
|
proto::GetImplementation {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetImplementation,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<LocationLink>,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
_: &clock::Global,
|
|
cx: &mut App,
|
|
) -> proto::GetImplementationResponse {
|
|
let links = location_links_to_proto(response, lsp_store, peer_id, cx);
|
|
proto::GetImplementationResponse { links }
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetImplementationResponse,
|
|
project: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
location_links_from_proto(message.links, project, cx).await
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetImplementation) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetTypeDefinitions {
|
|
type Response = Vec<LocationLink>;
|
|
type LspRequest = lsp::request::GotoTypeDefinition;
|
|
type ProtoRequest = proto::GetTypeDefinition;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get type definition"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
!matches!(
|
|
&capabilities.server_capabilities.type_definition_provider,
|
|
None | Some(lsp::TypeDefinitionProviderCapability::Simple(false))
|
|
)
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::GotoTypeDefinitionParams> {
|
|
Ok(lsp::GotoTypeDefinitionParams {
|
|
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::GotoTypeDefinitionResponse>,
|
|
project: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
location_links_from_lsp(message, project, buffer, server_id, cx).await
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition {
|
|
proto::GetTypeDefinition {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetTypeDefinition,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<LocationLink>,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
_: &clock::Global,
|
|
cx: &mut App,
|
|
) -> proto::GetTypeDefinitionResponse {
|
|
let links = location_links_to_proto(response, lsp_store, peer_id, cx);
|
|
proto::GetTypeDefinitionResponse { links }
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetTypeDefinitionResponse,
|
|
project: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
location_links_from_proto(message.links, project, cx).await
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
fn language_server_for_buffer(
|
|
lsp_store: &Entity<LspStore>,
|
|
buffer: &Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<(Arc<CachedLspAdapter>, Arc<LanguageServer>)> {
|
|
lsp_store
|
|
.update(cx, |lsp_store, cx| {
|
|
buffer.update(cx, |buffer, cx| {
|
|
lsp_store
|
|
.language_server_for_local_buffer(buffer, server_id, cx)
|
|
.map(|(adapter, server)| (adapter.clone(), server.clone()))
|
|
})
|
|
})?
|
|
.context("no language server found for buffer")
|
|
}
|
|
|
|
pub async fn location_links_from_proto(
|
|
proto_links: Vec<proto::LocationLink>,
|
|
lsp_store: Entity<LspStore>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
let mut links = Vec::new();
|
|
|
|
for link in proto_links {
|
|
links.push(location_link_from_proto(link, lsp_store.clone(), &mut cx).await?)
|
|
}
|
|
|
|
Ok(links)
|
|
}
|
|
|
|
pub fn location_link_from_proto(
|
|
link: proto::LocationLink,
|
|
lsp_store: Entity<LspStore>,
|
|
cx: &mut AsyncApp,
|
|
) -> Task<Result<LocationLink>> {
|
|
cx.spawn(async move |cx| {
|
|
let origin = match link.origin {
|
|
Some(origin) => {
|
|
let buffer_id = BufferId::new(origin.buffer_id)?;
|
|
let buffer = lsp_store
|
|
.update(cx, |lsp_store, cx| {
|
|
lsp_store.wait_for_remote_buffer(buffer_id, cx)
|
|
})?
|
|
.await?;
|
|
let start = origin
|
|
.start
|
|
.and_then(deserialize_anchor)
|
|
.context("missing origin start")?;
|
|
let end = origin
|
|
.end
|
|
.and_then(deserialize_anchor)
|
|
.context("missing origin end")?;
|
|
buffer
|
|
.update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
|
|
.await?;
|
|
Some(Location {
|
|
buffer,
|
|
range: start..end,
|
|
})
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let target = link.target.context("missing target")?;
|
|
let buffer_id = BufferId::new(target.buffer_id)?;
|
|
let buffer = lsp_store
|
|
.update(cx, |lsp_store, cx| {
|
|
lsp_store.wait_for_remote_buffer(buffer_id, cx)
|
|
})?
|
|
.await?;
|
|
let start = target
|
|
.start
|
|
.and_then(deserialize_anchor)
|
|
.context("missing target start")?;
|
|
let end = target
|
|
.end
|
|
.and_then(deserialize_anchor)
|
|
.context("missing target end")?;
|
|
buffer
|
|
.update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
|
|
.await?;
|
|
let target = Location {
|
|
buffer,
|
|
range: start..end,
|
|
};
|
|
Ok(LocationLink { origin, target })
|
|
})
|
|
}
|
|
|
|
pub async fn location_links_from_lsp(
|
|
message: Option<lsp::GotoDefinitionResponse>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Vec<LocationLink>> {
|
|
let message = match message {
|
|
Some(message) => message,
|
|
None => return Ok(Vec::new()),
|
|
};
|
|
|
|
let mut unresolved_links = Vec::new();
|
|
match message {
|
|
lsp::GotoDefinitionResponse::Scalar(loc) => {
|
|
unresolved_links.push((None, loc.uri, loc.range));
|
|
}
|
|
|
|
lsp::GotoDefinitionResponse::Array(locs) => {
|
|
unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
|
|
}
|
|
|
|
lsp::GotoDefinitionResponse::Link(links) => {
|
|
unresolved_links.extend(links.into_iter().map(|l| {
|
|
(
|
|
l.origin_selection_range,
|
|
l.target_uri,
|
|
l.target_selection_range,
|
|
)
|
|
}));
|
|
}
|
|
}
|
|
|
|
let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
|
let mut definitions = Vec::new();
|
|
for (origin_range, target_uri, target_range) in unresolved_links {
|
|
let target_buffer_handle = lsp_store
|
|
.update(&mut cx, |this, cx| {
|
|
this.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx)
|
|
})?
|
|
.await?;
|
|
|
|
cx.update(|cx| {
|
|
let origin_location = origin_range.map(|origin_range| {
|
|
let origin_buffer = buffer.read(cx);
|
|
let origin_start =
|
|
origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
|
|
let origin_end =
|
|
origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left);
|
|
Location {
|
|
buffer: buffer.clone(),
|
|
range: origin_buffer.anchor_after(origin_start)
|
|
..origin_buffer.anchor_before(origin_end),
|
|
}
|
|
});
|
|
|
|
let target_buffer = target_buffer_handle.read(cx);
|
|
let target_start =
|
|
target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
|
|
let target_end =
|
|
target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
|
|
let target_location = Location {
|
|
buffer: target_buffer_handle,
|
|
range: target_buffer.anchor_after(target_start)
|
|
..target_buffer.anchor_before(target_end),
|
|
};
|
|
|
|
definitions.push(LocationLink {
|
|
origin: origin_location,
|
|
target: target_location,
|
|
})
|
|
})?;
|
|
}
|
|
Ok(definitions)
|
|
}
|
|
|
|
pub async fn location_link_from_lsp(
|
|
link: lsp::LocationLink,
|
|
lsp_store: &Entity<LspStore>,
|
|
buffer: &Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: &mut AsyncApp,
|
|
) -> Result<LocationLink> {
|
|
let (_, language_server) = language_server_for_buffer(lsp_store, buffer, server_id, cx)?;
|
|
|
|
let (origin_range, target_uri, target_range) = (
|
|
link.origin_selection_range,
|
|
link.target_uri,
|
|
link.target_selection_range,
|
|
);
|
|
|
|
let target_buffer_handle = lsp_store
|
|
.update(cx, |lsp_store, cx| {
|
|
lsp_store.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx)
|
|
})?
|
|
.await?;
|
|
|
|
cx.update(|cx| {
|
|
let origin_location = origin_range.map(|origin_range| {
|
|
let origin_buffer = buffer.read(cx);
|
|
let origin_start =
|
|
origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left);
|
|
let origin_end =
|
|
origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left);
|
|
Location {
|
|
buffer: buffer.clone(),
|
|
range: origin_buffer.anchor_after(origin_start)
|
|
..origin_buffer.anchor_before(origin_end),
|
|
}
|
|
});
|
|
|
|
let target_buffer = target_buffer_handle.read(cx);
|
|
let target_start =
|
|
target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left);
|
|
let target_end =
|
|
target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left);
|
|
let target_location = Location {
|
|
buffer: target_buffer_handle,
|
|
range: target_buffer.anchor_after(target_start)
|
|
..target_buffer.anchor_before(target_end),
|
|
};
|
|
|
|
LocationLink {
|
|
origin: origin_location,
|
|
target: target_location,
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn location_links_to_proto(
|
|
links: Vec<LocationLink>,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
cx: &mut App,
|
|
) -> Vec<proto::LocationLink> {
|
|
links
|
|
.into_iter()
|
|
.map(|definition| location_link_to_proto(definition, lsp_store, peer_id, cx))
|
|
.collect()
|
|
}
|
|
|
|
pub fn location_link_to_proto(
|
|
location: LocationLink,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
cx: &mut App,
|
|
) -> proto::LocationLink {
|
|
let origin = location.origin.map(|origin| {
|
|
lsp_store
|
|
.buffer_store()
|
|
.update(cx, |buffer_store, cx| {
|
|
buffer_store.create_buffer_for_peer(&origin.buffer, peer_id, cx)
|
|
})
|
|
.detach_and_log_err(cx);
|
|
|
|
let buffer_id = origin.buffer.read(cx).remote_id().into();
|
|
proto::Location {
|
|
start: Some(serialize_anchor(&origin.range.start)),
|
|
end: Some(serialize_anchor(&origin.range.end)),
|
|
buffer_id,
|
|
}
|
|
});
|
|
|
|
lsp_store
|
|
.buffer_store()
|
|
.update(cx, |buffer_store, cx| {
|
|
buffer_store.create_buffer_for_peer(&location.target.buffer, peer_id, cx)
|
|
})
|
|
.detach_and_log_err(cx);
|
|
|
|
let buffer_id = location.target.buffer.read(cx).remote_id().into();
|
|
let target = proto::Location {
|
|
start: Some(serialize_anchor(&location.target.range.start)),
|
|
end: Some(serialize_anchor(&location.target.range.end)),
|
|
buffer_id,
|
|
};
|
|
|
|
proto::LocationLink {
|
|
origin,
|
|
target: Some(target),
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetReferences {
|
|
type Response = Vec<Location>;
|
|
type LspRequest = lsp::request::References;
|
|
type ProtoRequest = proto::GetReferences;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Find all references"
|
|
}
|
|
|
|
fn status(&self) -> Option<String> {
|
|
Some("Finding references...".to_owned())
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
match &capabilities.server_capabilities.references_provider {
|
|
Some(OneOf::Left(has_support)) => *has_support,
|
|
Some(OneOf::Right(_)) => true,
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::ReferenceParams> {
|
|
Ok(lsp::ReferenceParams {
|
|
text_document_position: make_lsp_text_document_position(path, self.position)?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
context: lsp::ReferenceContext {
|
|
include_declaration: true,
|
|
},
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
locations: Option<Vec<lsp::Location>>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Vec<Location>> {
|
|
let mut references = Vec::new();
|
|
let (_, language_server) =
|
|
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
|
|
|
if let Some(locations) = locations {
|
|
for lsp_location in locations {
|
|
let target_buffer_handle = lsp_store
|
|
.update(&mut cx, |lsp_store, cx| {
|
|
lsp_store.open_local_buffer_via_lsp(
|
|
lsp_location.uri,
|
|
language_server.server_id(),
|
|
cx,
|
|
)
|
|
})?
|
|
.await?;
|
|
|
|
target_buffer_handle
|
|
.clone()
|
|
.read_with(&cx, |target_buffer, _| {
|
|
let target_start = target_buffer
|
|
.clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left);
|
|
let target_end = target_buffer
|
|
.clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left);
|
|
references.push(Location {
|
|
buffer: target_buffer_handle,
|
|
range: target_buffer.anchor_after(target_start)
|
|
..target_buffer.anchor_before(target_end),
|
|
});
|
|
})?;
|
|
}
|
|
}
|
|
|
|
Ok(references)
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetReferences {
|
|
proto::GetReferences {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetReferences,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<Location>,
|
|
lsp_store: &mut LspStore,
|
|
peer_id: PeerId,
|
|
_: &clock::Global,
|
|
cx: &mut App,
|
|
) -> proto::GetReferencesResponse {
|
|
let locations = response
|
|
.into_iter()
|
|
.map(|definition| {
|
|
lsp_store
|
|
.buffer_store()
|
|
.update(cx, |buffer_store, cx| {
|
|
buffer_store.create_buffer_for_peer(&definition.buffer, peer_id, cx)
|
|
})
|
|
.detach_and_log_err(cx);
|
|
let buffer_id = definition.buffer.read(cx).remote_id();
|
|
proto::Location {
|
|
start: Some(serialize_anchor(&definition.range.start)),
|
|
end: Some(serialize_anchor(&definition.range.end)),
|
|
buffer_id: buffer_id.into(),
|
|
}
|
|
})
|
|
.collect();
|
|
proto::GetReferencesResponse { locations }
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetReferencesResponse,
|
|
project: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Vec<Location>> {
|
|
let mut locations = Vec::new();
|
|
for location in message.locations {
|
|
let buffer_id = BufferId::new(location.buffer_id)?;
|
|
let target_buffer = project
|
|
.update(&mut cx, |this, cx| {
|
|
this.wait_for_remote_buffer(buffer_id, cx)
|
|
})?
|
|
.await?;
|
|
let start = location
|
|
.start
|
|
.and_then(deserialize_anchor)
|
|
.context("missing target start")?;
|
|
let end = location
|
|
.end
|
|
.and_then(deserialize_anchor)
|
|
.context("missing target end")?;
|
|
target_buffer
|
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
|
|
.await?;
|
|
locations.push(Location {
|
|
buffer: target_buffer,
|
|
range: start..end,
|
|
})
|
|
}
|
|
Ok(locations)
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetReferences) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetDocumentHighlights {
|
|
type Response = Vec<DocumentHighlight>;
|
|
type LspRequest = lsp::request::DocumentHighlightRequest;
|
|
type ProtoRequest = proto::GetDocumentHighlights;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get document highlights"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.document_highlight_provider
|
|
.is_some_and(|capability| match capability {
|
|
OneOf::Left(supported) => supported,
|
|
OneOf::Right(_options) => true,
|
|
})
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::DocumentHighlightParams> {
|
|
Ok(lsp::DocumentHighlightParams {
|
|
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
lsp_highlights: Option<Vec<lsp::DocumentHighlight>>,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
_: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<DocumentHighlight>> {
|
|
buffer.read_with(&cx, |buffer, _| {
|
|
let mut lsp_highlights = lsp_highlights.unwrap_or_default();
|
|
lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end)));
|
|
lsp_highlights
|
|
.into_iter()
|
|
.map(|lsp_highlight| {
|
|
let start = buffer
|
|
.clip_point_utf16(point_from_lsp(lsp_highlight.range.start), Bias::Left);
|
|
let end = buffer
|
|
.clip_point_utf16(point_from_lsp(lsp_highlight.range.end), Bias::Left);
|
|
DocumentHighlight {
|
|
range: buffer.anchor_after(start)..buffer.anchor_before(end),
|
|
kind: lsp_highlight
|
|
.kind
|
|
.unwrap_or(lsp::DocumentHighlightKind::READ),
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentHighlights {
|
|
proto::GetDocumentHighlights {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetDocumentHighlights,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<DocumentHighlight>,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
_: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::GetDocumentHighlightsResponse {
|
|
let highlights = response
|
|
.into_iter()
|
|
.map(|highlight| proto::DocumentHighlight {
|
|
start: Some(serialize_anchor(&highlight.range.start)),
|
|
end: Some(serialize_anchor(&highlight.range.end)),
|
|
kind: match highlight.kind {
|
|
DocumentHighlightKind::TEXT => proto::document_highlight::Kind::Text.into(),
|
|
DocumentHighlightKind::WRITE => proto::document_highlight::Kind::Write.into(),
|
|
DocumentHighlightKind::READ => proto::document_highlight::Kind::Read.into(),
|
|
_ => proto::document_highlight::Kind::Text.into(),
|
|
},
|
|
})
|
|
.collect();
|
|
proto::GetDocumentHighlightsResponse { highlights }
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetDocumentHighlightsResponse,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Vec<DocumentHighlight>> {
|
|
let mut highlights = Vec::new();
|
|
for highlight in message.highlights {
|
|
let start = highlight
|
|
.start
|
|
.and_then(deserialize_anchor)
|
|
.context("missing target start")?;
|
|
let end = highlight
|
|
.end
|
|
.and_then(deserialize_anchor)
|
|
.context("missing target end")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))?
|
|
.await?;
|
|
let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) {
|
|
Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT,
|
|
Some(proto::document_highlight::Kind::Read) => DocumentHighlightKind::READ,
|
|
Some(proto::document_highlight::Kind::Write) => DocumentHighlightKind::WRITE,
|
|
None => DocumentHighlightKind::TEXT,
|
|
};
|
|
highlights.push(DocumentHighlight {
|
|
range: start..end,
|
|
kind,
|
|
});
|
|
}
|
|
Ok(highlights)
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetDocumentHighlights) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetDocumentSymbols {
|
|
type Response = Vec<DocumentSymbol>;
|
|
type LspRequest = lsp::request::DocumentSymbolRequest;
|
|
type ProtoRequest = proto::GetDocumentSymbols;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get document symbols"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.document_symbol_provider
|
|
.is_some_and(|capability| match capability {
|
|
OneOf::Left(supported) => supported,
|
|
OneOf::Right(_options) => true,
|
|
})
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::DocumentSymbolParams> {
|
|
Ok(lsp::DocumentSymbolParams {
|
|
text_document: make_text_document_identifier(path)?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
lsp_symbols: Option<lsp::DocumentSymbolResponse>,
|
|
_: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: LanguageServerId,
|
|
_: AsyncApp,
|
|
) -> Result<Vec<DocumentSymbol>> {
|
|
let Some(lsp_symbols) = lsp_symbols else {
|
|
return Ok(Vec::new());
|
|
};
|
|
|
|
let symbols: Vec<_> = match lsp_symbols {
|
|
lsp::DocumentSymbolResponse::Flat(symbol_information) => symbol_information
|
|
.into_iter()
|
|
.map(|lsp_symbol| DocumentSymbol {
|
|
name: lsp_symbol.name,
|
|
kind: lsp_symbol.kind,
|
|
range: range_from_lsp(lsp_symbol.location.range),
|
|
selection_range: range_from_lsp(lsp_symbol.location.range),
|
|
children: Vec::new(),
|
|
})
|
|
.collect(),
|
|
lsp::DocumentSymbolResponse::Nested(nested_responses) => {
|
|
fn convert_symbol(lsp_symbol: lsp::DocumentSymbol) -> DocumentSymbol {
|
|
DocumentSymbol {
|
|
name: lsp_symbol.name,
|
|
kind: lsp_symbol.kind,
|
|
range: range_from_lsp(lsp_symbol.range),
|
|
selection_range: range_from_lsp(lsp_symbol.selection_range),
|
|
children: lsp_symbol
|
|
.children
|
|
.map(|children| {
|
|
children.into_iter().map(convert_symbol).collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default(),
|
|
}
|
|
}
|
|
nested_responses.into_iter().map(convert_symbol).collect()
|
|
}
|
|
};
|
|
Ok(symbols)
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentSymbols {
|
|
proto::GetDocumentSymbols {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetDocumentSymbols,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self)
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<DocumentSymbol>,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
_: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::GetDocumentSymbolsResponse {
|
|
let symbols = response
|
|
.into_iter()
|
|
.map(|symbol| {
|
|
fn convert_symbol_to_proto(symbol: DocumentSymbol) -> proto::DocumentSymbol {
|
|
proto::DocumentSymbol {
|
|
name: symbol.name.clone(),
|
|
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
|
|
start: Some(proto::PointUtf16 {
|
|
row: symbol.range.start.0.row,
|
|
column: symbol.range.start.0.column,
|
|
}),
|
|
end: Some(proto::PointUtf16 {
|
|
row: symbol.range.end.0.row,
|
|
column: symbol.range.end.0.column,
|
|
}),
|
|
selection_start: Some(proto::PointUtf16 {
|
|
row: symbol.selection_range.start.0.row,
|
|
column: symbol.selection_range.start.0.column,
|
|
}),
|
|
selection_end: Some(proto::PointUtf16 {
|
|
row: symbol.selection_range.end.0.row,
|
|
column: symbol.selection_range.end.0.column,
|
|
}),
|
|
children: symbol
|
|
.children
|
|
.into_iter()
|
|
.map(convert_symbol_to_proto)
|
|
.collect(),
|
|
}
|
|
}
|
|
convert_symbol_to_proto(symbol)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
proto::GetDocumentSymbolsResponse { symbols }
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetDocumentSymbolsResponse,
|
|
_: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: AsyncApp,
|
|
) -> Result<Vec<DocumentSymbol>> {
|
|
let mut symbols = Vec::with_capacity(message.symbols.len());
|
|
for serialized_symbol in message.symbols {
|
|
fn deserialize_symbol_with_children(
|
|
serialized_symbol: proto::DocumentSymbol,
|
|
) -> Result<DocumentSymbol> {
|
|
let kind =
|
|
unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
|
|
|
|
let start = serialized_symbol.start.context("invalid start")?;
|
|
let end = serialized_symbol.end.context("invalid end")?;
|
|
|
|
let selection_start = serialized_symbol
|
|
.selection_start
|
|
.context("invalid selection start")?;
|
|
let selection_end = serialized_symbol
|
|
.selection_end
|
|
.context("invalid selection end")?;
|
|
|
|
Ok(DocumentSymbol {
|
|
name: serialized_symbol.name,
|
|
kind,
|
|
range: Unclipped(PointUtf16::new(start.row, start.column))
|
|
..Unclipped(PointUtf16::new(end.row, end.column)),
|
|
selection_range: Unclipped(PointUtf16::new(
|
|
selection_start.row,
|
|
selection_start.column,
|
|
))
|
|
..Unclipped(PointUtf16::new(selection_end.row, selection_end.column)),
|
|
children: serialized_symbol
|
|
.children
|
|
.into_iter()
|
|
.filter_map(|symbol| deserialize_symbol_with_children(symbol).ok())
|
|
.collect::<Vec<_>>(),
|
|
})
|
|
}
|
|
|
|
symbols.push(deserialize_symbol_with_children(serialized_symbol)?);
|
|
}
|
|
|
|
Ok(symbols)
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetDocumentSymbols) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetSignatureHelp {
|
|
type Response = Option<SignatureHelp>;
|
|
type LspRequest = lsp::SignatureHelpRequest;
|
|
type ProtoRequest = proto::GetSignatureHelp;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get signature help"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.signature_help_provider
|
|
.is_some()
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_cx: &App,
|
|
) -> Result<lsp::SignatureHelpParams> {
|
|
Ok(lsp::SignatureHelpParams {
|
|
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
|
context: None,
|
|
work_done_progress_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::SignatureHelp>,
|
|
lsp_store: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
let Some(message) = message else {
|
|
return Ok(None);
|
|
};
|
|
cx.update(|cx| SignatureHelp::new(message, Some(lsp_store.read(cx).languages.clone()), cx))
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
|
|
let offset = buffer.point_utf16_to_offset(self.position);
|
|
proto::GetSignatureHelp {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().to_proto(),
|
|
position: Some(serialize_anchor(&buffer.anchor_after(offset))),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
payload: Self::ProtoRequest,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&payload.version))
|
|
})?
|
|
.await
|
|
.with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?;
|
|
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?;
|
|
Ok(Self {
|
|
position: payload
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?
|
|
.to_point_utf16(&buffer_snapshot),
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Self::Response,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
_: &Global,
|
|
_: &mut App,
|
|
) -> proto::GetSignatureHelpResponse {
|
|
proto::GetSignatureHelpResponse {
|
|
signature_help: response
|
|
.map(|signature_help| lsp_to_proto_signature(signature_help.original_data)),
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
response: proto::GetSignatureHelpResponse,
|
|
lsp_store: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
cx: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
cx.update(|cx| {
|
|
response
|
|
.signature_help
|
|
.map(proto_to_lsp_signature)
|
|
.and_then(|signature| {
|
|
SignatureHelp::new(signature, Some(lsp_store.read(cx).languages.clone()), cx)
|
|
})
|
|
})
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetHover {
|
|
type Response = Option<Hover>;
|
|
type LspRequest = lsp::request::HoverRequest;
|
|
type ProtoRequest = proto::GetHover;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get hover"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
match capabilities.server_capabilities.hover_provider {
|
|
Some(lsp::HoverProviderCapability::Simple(enabled)) => enabled,
|
|
Some(lsp::HoverProviderCapability::Options(_)) => true,
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::HoverParams> {
|
|
Ok(lsp::HoverParams {
|
|
text_document_position_params: make_lsp_text_document_position(path, self.position)?,
|
|
work_done_progress_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::Hover>,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
_: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
let Some(hover) = message else {
|
|
return Ok(None);
|
|
};
|
|
|
|
let (language, range) = buffer.read_with(&cx, |buffer, _| {
|
|
(
|
|
buffer.language().cloned(),
|
|
hover.range.map(|range| {
|
|
let token_start =
|
|
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
|
|
let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
|
|
buffer.anchor_after(token_start)..buffer.anchor_before(token_end)
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
fn hover_blocks_from_marked_string(marked_string: lsp::MarkedString) -> Option<HoverBlock> {
|
|
let block = match marked_string {
|
|
lsp::MarkedString::String(content) => HoverBlock {
|
|
text: content,
|
|
kind: HoverBlockKind::Markdown,
|
|
},
|
|
lsp::MarkedString::LanguageString(lsp::LanguageString { language, value }) => {
|
|
HoverBlock {
|
|
text: value,
|
|
kind: HoverBlockKind::Code { language },
|
|
}
|
|
}
|
|
};
|
|
if block.text.is_empty() {
|
|
None
|
|
} else {
|
|
Some(block)
|
|
}
|
|
}
|
|
|
|
let contents = match hover.contents {
|
|
lsp::HoverContents::Scalar(marked_string) => {
|
|
hover_blocks_from_marked_string(marked_string)
|
|
.into_iter()
|
|
.collect()
|
|
}
|
|
lsp::HoverContents::Array(marked_strings) => marked_strings
|
|
.into_iter()
|
|
.filter_map(hover_blocks_from_marked_string)
|
|
.collect(),
|
|
lsp::HoverContents::Markup(markup_content) => vec![HoverBlock {
|
|
text: markup_content.value,
|
|
kind: if markup_content.kind == lsp::MarkupKind::Markdown {
|
|
HoverBlockKind::Markdown
|
|
} else {
|
|
HoverBlockKind::PlainText
|
|
},
|
|
}],
|
|
};
|
|
|
|
Ok(Some(Hover {
|
|
contents,
|
|
range,
|
|
language,
|
|
}))
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
|
|
proto::GetHover {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
version: serialize_version(&buffer.version),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: Self::ProtoRequest,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Self::Response,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
_: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::GetHoverResponse {
|
|
if let Some(response) = response {
|
|
let (start, end) = if let Some(range) = response.range {
|
|
(
|
|
Some(language::proto::serialize_anchor(&range.start)),
|
|
Some(language::proto::serialize_anchor(&range.end)),
|
|
)
|
|
} else {
|
|
(None, None)
|
|
};
|
|
|
|
let contents = response
|
|
.contents
|
|
.into_iter()
|
|
.map(|block| proto::HoverBlock {
|
|
text: block.text,
|
|
is_markdown: block.kind == HoverBlockKind::Markdown,
|
|
language: if let HoverBlockKind::Code { language } = block.kind {
|
|
Some(language)
|
|
} else {
|
|
None
|
|
},
|
|
})
|
|
.collect();
|
|
|
|
proto::GetHoverResponse {
|
|
start,
|
|
end,
|
|
contents,
|
|
}
|
|
} else {
|
|
proto::GetHoverResponse {
|
|
start: None,
|
|
end: None,
|
|
contents: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetHoverResponse,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
let contents: Vec<_> = message
|
|
.contents
|
|
.into_iter()
|
|
.map(|block| HoverBlock {
|
|
text: block.text,
|
|
kind: if let Some(language) = block.language {
|
|
HoverBlockKind::Code { language }
|
|
} else if block.is_markdown {
|
|
HoverBlockKind::Markdown
|
|
} else {
|
|
HoverBlockKind::PlainText
|
|
},
|
|
})
|
|
.collect();
|
|
if contents.is_empty() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned())?;
|
|
let range = if let (Some(start), Some(end)) = (message.start, message.end) {
|
|
language::proto::deserialize_anchor(start)
|
|
.and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end))
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(range) = range.as_ref() {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_anchors([range.start, range.end])
|
|
})?
|
|
.await?;
|
|
}
|
|
|
|
Ok(Some(Hover {
|
|
contents,
|
|
range,
|
|
language,
|
|
}))
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
impl GetCompletions {
|
|
pub fn can_resolve_completions(capabilities: &lsp::ServerCapabilities) -> bool {
|
|
capabilities
|
|
.completion_provider
|
|
.as_ref()
|
|
.and_then(|options| options.resolve_provider)
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetCompletions {
|
|
type Response = CoreCompletionResponse;
|
|
type LspRequest = lsp::request::Completion;
|
|
type ProtoRequest = proto::GetCompletions;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get completion"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.completion_provider
|
|
.is_some()
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::CompletionParams> {
|
|
Ok(lsp::CompletionParams {
|
|
text_document_position: make_lsp_text_document_position(path, self.position)?,
|
|
context: Some(self.context.clone()),
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
completions: Option<lsp::CompletionResponse>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
let mut response_list = None;
|
|
let (mut completions, mut is_incomplete) = if let Some(completions) = completions {
|
|
match completions {
|
|
lsp::CompletionResponse::Array(completions) => (completions, false),
|
|
lsp::CompletionResponse::List(mut list) => {
|
|
let is_incomplete = list.is_incomplete;
|
|
let items = std::mem::take(&mut list.items);
|
|
response_list = Some(list);
|
|
(items, is_incomplete)
|
|
}
|
|
}
|
|
} else {
|
|
(Vec::new(), false)
|
|
};
|
|
|
|
let unfiltered_completions_count = completions.len();
|
|
|
|
let language_server_adapter = lsp_store
|
|
.read_with(&cx, |lsp_store, _| {
|
|
lsp_store.language_server_adapter_for_id(server_id)
|
|
})?
|
|
.with_context(|| format!("no language server with id {server_id}"))?;
|
|
|
|
let lsp_defaults = response_list
|
|
.as_ref()
|
|
.and_then(|list| list.item_defaults.clone())
|
|
.map(Arc::new);
|
|
|
|
let mut completion_edits = Vec::new();
|
|
buffer.update(&mut cx, |buffer, _cx| {
|
|
let snapshot = buffer.snapshot();
|
|
let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
|
|
|
|
let mut range_for_token = None;
|
|
completions.retain(|lsp_completion| {
|
|
let lsp_edit = lsp_completion.text_edit.clone().or_else(|| {
|
|
let default_text_edit = lsp_defaults.as_deref()?.edit_range.as_ref()?;
|
|
let new_text = lsp_completion
|
|
.insert_text
|
|
.as_ref()
|
|
.unwrap_or(&lsp_completion.label)
|
|
.clone();
|
|
match default_text_edit {
|
|
CompletionListItemDefaultsEditRange::Range(range) => {
|
|
Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
|
range: *range,
|
|
new_text,
|
|
}))
|
|
}
|
|
CompletionListItemDefaultsEditRange::InsertAndReplace {
|
|
insert,
|
|
replace,
|
|
} => Some(lsp::CompletionTextEdit::InsertAndReplace(
|
|
lsp::InsertReplaceEdit {
|
|
new_text,
|
|
insert: *insert,
|
|
replace: *replace,
|
|
},
|
|
)),
|
|
}
|
|
});
|
|
|
|
let edit = match lsp_edit {
|
|
// If the language server provides a range to overwrite, then
|
|
// check that the range is valid.
|
|
Some(completion_text_edit) => {
|
|
match parse_completion_text_edit(&completion_text_edit, &snapshot) {
|
|
Some(edit) => edit,
|
|
None => return false,
|
|
}
|
|
}
|
|
// 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 false;
|
|
}
|
|
|
|
let default_edit_range = lsp_defaults.as_ref().and_then(|lsp_defaults| {
|
|
lsp_defaults
|
|
.edit_range
|
|
.as_ref()
|
|
.and_then(|range| match range {
|
|
CompletionListItemDefaultsEditRange::Range(r) => Some(r),
|
|
_ => None,
|
|
})
|
|
});
|
|
|
|
let range = if let Some(range) = default_edit_range {
|
|
let range = range_from_lsp(*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 false;
|
|
}
|
|
|
|
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
|
} else {
|
|
range_for_token
|
|
.get_or_insert_with(|| {
|
|
let offset = self.position.to_offset(&snapshot);
|
|
let (range, kind) = snapshot.surrounding_word(offset, true);
|
|
let range = if kind == Some(CharKind::Word) {
|
|
range
|
|
} else {
|
|
offset..offset
|
|
};
|
|
|
|
snapshot.anchor_before(range.start)
|
|
..snapshot.anchor_after(range.end)
|
|
})
|
|
.clone()
|
|
};
|
|
|
|
// We already know text_edit is None here
|
|
let text = lsp_completion
|
|
.insert_text
|
|
.as_ref()
|
|
.unwrap_or(&lsp_completion.label)
|
|
.clone();
|
|
|
|
ParsedCompletionEdit {
|
|
replace_range: range,
|
|
insert_range: None,
|
|
new_text: text,
|
|
}
|
|
}
|
|
};
|
|
|
|
completion_edits.push(edit);
|
|
true
|
|
});
|
|
})?;
|
|
|
|
// If completions were filtered out due to errors that may be transient, mark the result
|
|
// incomplete so that it is re-queried.
|
|
if unfiltered_completions_count != completions.len() {
|
|
is_incomplete = true;
|
|
}
|
|
|
|
language_server_adapter
|
|
.process_completions(&mut completions)
|
|
.await;
|
|
|
|
let completions = completions
|
|
.into_iter()
|
|
.zip(completion_edits)
|
|
.map(|(mut lsp_completion, mut edit)| {
|
|
LineEnding::normalize(&mut edit.new_text);
|
|
if lsp_completion.data.is_none()
|
|
&& let Some(default_data) = lsp_defaults
|
|
.as_ref()
|
|
.and_then(|item_defaults| item_defaults.data.clone())
|
|
{
|
|
// Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later,
|
|
// so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception.
|
|
lsp_completion.data = Some(default_data);
|
|
}
|
|
CoreCompletion {
|
|
replace_range: edit.replace_range,
|
|
new_text: edit.new_text,
|
|
source: CompletionSource::Lsp {
|
|
insert_range: edit.insert_range,
|
|
server_id,
|
|
lsp_completion: Box::new(lsp_completion),
|
|
lsp_defaults: lsp_defaults.clone(),
|
|
resolved: false,
|
|
},
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(CoreCompletionResponse {
|
|
completions,
|
|
is_incomplete,
|
|
})
|
|
}
|
|
|
|
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().into(),
|
|
position: Some(language::proto::serialize_anchor(&anchor)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetCompletions,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> 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)
|
|
})
|
|
})
|
|
.context("invalid position")??;
|
|
Ok(Self {
|
|
position,
|
|
context: CompletionContext {
|
|
trigger_kind: CompletionTriggerKind::INVOKED,
|
|
trigger_character: None,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: CoreCompletionResponse,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
buffer_version: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::GetCompletionsResponse {
|
|
proto::GetCompletionsResponse {
|
|
completions: response
|
|
.completions
|
|
.iter()
|
|
.map(LspStore::serialize_completion)
|
|
.collect(),
|
|
version: serialize_version(buffer_version),
|
|
can_reuse: !response.is_incomplete,
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetCompletionsResponse,
|
|
_project: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
|
|
let completions = message
|
|
.completions
|
|
.into_iter()
|
|
.map(LspStore::deserialize_completion)
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
Ok(CoreCompletionResponse {
|
|
completions,
|
|
is_incomplete: !message.can_reuse,
|
|
})
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetCompletions) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
pub struct ParsedCompletionEdit {
|
|
pub replace_range: Range<Anchor>,
|
|
pub insert_range: Option<Range<Anchor>>,
|
|
pub new_text: String,
|
|
}
|
|
|
|
pub(crate) fn parse_completion_text_edit(
|
|
edit: &lsp::CompletionTextEdit,
|
|
snapshot: &BufferSnapshot,
|
|
) -> Option<ParsedCompletionEdit> {
|
|
let (replace_range, insert_range, new_text) = match edit {
|
|
lsp::CompletionTextEdit::Edit(edit) => (edit.range, None, &edit.new_text),
|
|
lsp::CompletionTextEdit::InsertAndReplace(edit) => {
|
|
(edit.replace, Some(edit.insert), &edit.new_text)
|
|
}
|
|
};
|
|
|
|
let replace_range = {
|
|
let range = range_from_lsp(replace_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, start: {start:?}, end: {end:?}, range: {range:?}"
|
|
);
|
|
return None;
|
|
}
|
|
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
|
};
|
|
|
|
let insert_range = match insert_range {
|
|
None => None,
|
|
Some(insert_range) => {
|
|
let range = range_from_lsp(insert_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 (insert) out of expected range");
|
|
return None;
|
|
}
|
|
Some(snapshot.anchor_before(start)..snapshot.anchor_after(end))
|
|
}
|
|
};
|
|
|
|
Some(ParsedCompletionEdit {
|
|
insert_range,
|
|
replace_range,
|
|
new_text: new_text.clone(),
|
|
})
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetCodeActions {
|
|
type Response = Vec<CodeAction>;
|
|
type LspRequest = lsp::request::CodeActionRequest;
|
|
type ProtoRequest = proto::GetCodeActions;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get code actions"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
match &capabilities.server_capabilities.code_action_provider {
|
|
None => false,
|
|
Some(lsp::CodeActionProviderCapability::Simple(false)) => false,
|
|
_ => {
|
|
// If we do know that we want specific code actions AND we know that
|
|
// the server only supports specific code actions, then we want to filter
|
|
// down to the ones that are supported.
|
|
if let Some((requested, supported)) = self
|
|
.kinds
|
|
.as_ref()
|
|
.zip(Self::supported_code_action_kinds(capabilities))
|
|
{
|
|
let server_supported = supported.into_iter().collect::<HashSet<_>>();
|
|
requested.iter().any(|kind| server_supported.contains(kind))
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
buffer: &Buffer,
|
|
language_server: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::CodeActionParams> {
|
|
let mut relevant_diagnostics = Vec::new();
|
|
for entry in buffer
|
|
.snapshot()
|
|
.diagnostics_in_range::<_, language::PointUtf16>(self.range.clone(), false)
|
|
{
|
|
relevant_diagnostics.push(entry.to_lsp_diagnostic_stub()?);
|
|
}
|
|
|
|
let supported =
|
|
Self::supported_code_action_kinds(language_server.adapter_server_capabilities());
|
|
|
|
let only = if let Some(requested) = &self.kinds {
|
|
if let Some(supported_kinds) = supported {
|
|
let server_supported = supported_kinds.into_iter().collect::<HashSet<_>>();
|
|
|
|
let filtered = requested
|
|
.iter()
|
|
.filter(|kind| server_supported.contains(kind))
|
|
.cloned()
|
|
.collect();
|
|
Some(filtered)
|
|
} else {
|
|
Some(requested.clone())
|
|
}
|
|
} else {
|
|
supported
|
|
};
|
|
|
|
Ok(lsp::CodeActionParams {
|
|
text_document: make_text_document_identifier(path)?,
|
|
range: range_to_lsp(self.range.to_point_utf16(buffer))?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
context: lsp::CodeActionContext {
|
|
diagnostics: relevant_diagnostics,
|
|
only,
|
|
..lsp::CodeActionContext::default()
|
|
},
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
actions: Option<lsp::CodeActionResponse>,
|
|
lsp_store: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<CodeAction>> {
|
|
let requested_kinds_set = self
|
|
.kinds
|
|
.map(|kinds| kinds.into_iter().collect::<HashSet<_>>());
|
|
|
|
let language_server = cx.update(|cx| {
|
|
lsp_store
|
|
.read(cx)
|
|
.language_server_for_id(server_id)
|
|
.with_context(|| {
|
|
format!("Missing the language server that just returned a response {server_id}")
|
|
})
|
|
})??;
|
|
|
|
let server_capabilities = language_server.capabilities();
|
|
let available_commands = server_capabilities
|
|
.execute_command_provider
|
|
.as_ref()
|
|
.map(|options| options.commands.as_slice())
|
|
.unwrap_or_default();
|
|
Ok(actions
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter_map(|entry| {
|
|
let (lsp_action, resolved) = match entry {
|
|
lsp::CodeActionOrCommand::CodeAction(lsp_action) => {
|
|
if let Some(command) = lsp_action.command.as_ref()
|
|
&& !available_commands.contains(&command.command)
|
|
{
|
|
return None;
|
|
}
|
|
(LspAction::Action(Box::new(lsp_action)), false)
|
|
}
|
|
lsp::CodeActionOrCommand::Command(command) => {
|
|
if available_commands.contains(&command.command) {
|
|
(LspAction::Command(command), true)
|
|
} else {
|
|
return None;
|
|
}
|
|
}
|
|
};
|
|
|
|
if let Some((requested_kinds, kind)) =
|
|
requested_kinds_set.as_ref().zip(lsp_action.action_kind())
|
|
&& !requested_kinds.contains(&kind)
|
|
{
|
|
return None;
|
|
}
|
|
|
|
Some(CodeAction {
|
|
server_id,
|
|
range: self.range.clone(),
|
|
lsp_action,
|
|
resolved,
|
|
})
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeActions {
|
|
proto::GetCodeActions {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
start: Some(language::proto::serialize_anchor(&self.range.start)),
|
|
end: Some(language::proto::serialize_anchor(&self.range.end)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetCodeActions,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let start = message
|
|
.start
|
|
.and_then(language::proto::deserialize_anchor)
|
|
.context("invalid start")?;
|
|
let end = message
|
|
.end
|
|
.and_then(language::proto::deserialize_anchor)
|
|
.context("invalid end")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
|
|
Ok(Self {
|
|
range: start..end,
|
|
kinds: None,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
code_actions: Vec<CodeAction>,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
buffer_version: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::GetCodeActionsResponse {
|
|
proto::GetCodeActionsResponse {
|
|
actions: code_actions
|
|
.iter()
|
|
.map(LspStore::serialize_code_action)
|
|
.collect(),
|
|
version: serialize_version(buffer_version),
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetCodeActionsResponse,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Vec<CodeAction>> {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
message
|
|
.actions
|
|
.into_iter()
|
|
.map(LspStore::deserialize_code_action)
|
|
.collect()
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetCodeActions) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
impl GetCodeActions {
|
|
fn supported_code_action_kinds(
|
|
capabilities: AdapterServerCapabilities,
|
|
) -> Option<Vec<CodeActionKind>> {
|
|
match capabilities.server_capabilities.code_action_provider {
|
|
Some(lsp::CodeActionProviderCapability::Options(CodeActionOptions {
|
|
code_action_kinds: Some(supported_action_kinds),
|
|
..
|
|
})) => Some(supported_action_kinds),
|
|
_ => capabilities.code_action_kinds,
|
|
}
|
|
}
|
|
|
|
pub fn can_resolve_actions(capabilities: &ServerCapabilities) -> bool {
|
|
capabilities
|
|
.code_action_provider
|
|
.as_ref()
|
|
.and_then(|options| match options {
|
|
lsp::CodeActionProviderCapability::Simple(_is_supported) => None,
|
|
lsp::CodeActionProviderCapability::Options(options) => options.resolve_provider,
|
|
})
|
|
.unwrap_or(false)
|
|
}
|
|
}
|
|
|
|
impl OnTypeFormatting {
|
|
pub fn supports_on_type_formatting(trigger: &str, capabilities: &ServerCapabilities) -> bool {
|
|
let Some(on_type_formatting_options) = &capabilities.document_on_type_formatting_provider
|
|
else {
|
|
return false;
|
|
};
|
|
on_type_formatting_options
|
|
.first_trigger_character
|
|
.contains(trigger)
|
|
|| on_type_formatting_options
|
|
.more_trigger_character
|
|
.iter()
|
|
.flatten()
|
|
.any(|chars| chars.contains(trigger))
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for OnTypeFormatting {
|
|
type Response = Option<Transaction>;
|
|
type LspRequest = lsp::request::OnTypeFormatting;
|
|
type ProtoRequest = proto::OnTypeFormatting;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Formatting on typing"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
Self::supports_on_type_formatting(&self.trigger, &capabilities.server_capabilities)
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::DocumentOnTypeFormattingParams> {
|
|
Ok(lsp::DocumentOnTypeFormattingParams {
|
|
text_document_position: make_lsp_text_document_position(path, self.position)?,
|
|
ch: self.trigger.clone(),
|
|
options: self.options.clone(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<Vec<lsp::TextEdit>>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Option<Transaction>> {
|
|
if let Some(edits) = message {
|
|
let (lsp_adapter, lsp_server) =
|
|
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
|
LocalLspStore::deserialize_text_edits(
|
|
lsp_store,
|
|
buffer,
|
|
edits,
|
|
self.push_to_history,
|
|
lsp_adapter,
|
|
lsp_server,
|
|
&mut cx,
|
|
)
|
|
.await
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting {
|
|
proto::OnTypeFormatting {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
position: Some(language::proto::serialize_anchor(
|
|
&buffer.anchor_before(self.position),
|
|
)),
|
|
trigger: self.trigger.clone(),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::OnTypeFormatting,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message
|
|
.position
|
|
.and_then(deserialize_anchor)
|
|
.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
|
|
let options = buffer.update(&mut cx, |buffer, cx| {
|
|
lsp_formatting_options(
|
|
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).as_ref(),
|
|
)
|
|
})?;
|
|
|
|
Ok(Self {
|
|
position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?,
|
|
trigger: message.trigger.clone(),
|
|
options,
|
|
push_to_history: false,
|
|
})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Option<Transaction>,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
_: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::OnTypeFormattingResponse {
|
|
proto::OnTypeFormattingResponse {
|
|
transaction: response
|
|
.map(|transaction| language::proto::serialize_transaction(&transaction)),
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::OnTypeFormattingResponse,
|
|
_: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: AsyncApp,
|
|
) -> Result<Option<Transaction>> {
|
|
let Some(transaction) = message.transaction else {
|
|
return Ok(None);
|
|
};
|
|
Ok(Some(language::proto::deserialize_transaction(transaction)?))
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
impl InlayHints {
|
|
pub async fn lsp_to_project_hint(
|
|
lsp_hint: lsp::InlayHint,
|
|
buffer_handle: &Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
resolve_state: ResolveState,
|
|
force_no_type_left_padding: bool,
|
|
cx: &mut AsyncApp,
|
|
) -> anyhow::Result<InlayHint> {
|
|
let kind = lsp_hint.kind.and_then(|kind| match kind {
|
|
lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
|
|
lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
|
|
_ => None,
|
|
});
|
|
|
|
let position = buffer_handle.read_with(cx, |buffer, _| {
|
|
let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
|
|
if kind == Some(InlayHintKind::Parameter) {
|
|
buffer.anchor_before(position)
|
|
} else {
|
|
buffer.anchor_after(position)
|
|
}
|
|
})?;
|
|
let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id)
|
|
.await
|
|
.context("lsp to project inlay hint conversion")?;
|
|
let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
|
|
false
|
|
} else {
|
|
lsp_hint.padding_left.unwrap_or(false)
|
|
};
|
|
|
|
Ok(InlayHint {
|
|
position,
|
|
padding_left,
|
|
padding_right: lsp_hint.padding_right.unwrap_or(false),
|
|
label,
|
|
kind,
|
|
tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
|
|
lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
|
|
lsp::InlayHintTooltip::MarkupContent(markup_content) => {
|
|
InlayHintTooltip::MarkupContent(MarkupContent {
|
|
kind: match markup_content.kind {
|
|
lsp::MarkupKind::PlainText => HoverBlockKind::PlainText,
|
|
lsp::MarkupKind::Markdown => HoverBlockKind::Markdown,
|
|
},
|
|
value: markup_content.value,
|
|
})
|
|
}
|
|
}),
|
|
resolve_state,
|
|
})
|
|
}
|
|
|
|
async fn lsp_inlay_label_to_project(
|
|
lsp_label: lsp::InlayHintLabel,
|
|
server_id: LanguageServerId,
|
|
) -> anyhow::Result<InlayHintLabel> {
|
|
let label = match lsp_label {
|
|
lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
|
|
lsp::InlayHintLabel::LabelParts(lsp_parts) => {
|
|
let mut parts = Vec::with_capacity(lsp_parts.len());
|
|
for lsp_part in lsp_parts {
|
|
parts.push(InlayHintLabelPart {
|
|
value: lsp_part.value,
|
|
tooltip: lsp_part.tooltip.map(|tooltip| match tooltip {
|
|
lsp::InlayHintLabelPartTooltip::String(s) => {
|
|
InlayHintLabelPartTooltip::String(s)
|
|
}
|
|
lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => {
|
|
InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
|
|
kind: match markup_content.kind {
|
|
lsp::MarkupKind::PlainText => HoverBlockKind::PlainText,
|
|
lsp::MarkupKind::Markdown => HoverBlockKind::Markdown,
|
|
},
|
|
value: markup_content.value,
|
|
})
|
|
}
|
|
}),
|
|
location: Some(server_id).zip(lsp_part.location),
|
|
});
|
|
}
|
|
InlayHintLabel::LabelParts(parts)
|
|
}
|
|
};
|
|
|
|
Ok(label)
|
|
}
|
|
|
|
pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint {
|
|
let (state, lsp_resolve_state) = match response_hint.resolve_state {
|
|
ResolveState::Resolved => (0, None),
|
|
ResolveState::CanResolve(server_id, resolve_data) => (
|
|
1,
|
|
Some(proto::resolve_state::LspResolveState {
|
|
server_id: server_id.0 as u64,
|
|
value: resolve_data.map(|json_data| {
|
|
serde_json::to_string(&json_data)
|
|
.expect("failed to serialize resolve json data")
|
|
}),
|
|
}),
|
|
),
|
|
ResolveState::Resolving => (2, None),
|
|
};
|
|
let resolve_state = Some(proto::ResolveState {
|
|
state,
|
|
lsp_resolve_state,
|
|
});
|
|
proto::InlayHint {
|
|
position: Some(language::proto::serialize_anchor(&response_hint.position)),
|
|
padding_left: response_hint.padding_left,
|
|
padding_right: response_hint.padding_right,
|
|
label: Some(proto::InlayHintLabel {
|
|
label: Some(match response_hint.label {
|
|
InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
|
|
InlayHintLabel::LabelParts(label_parts) => {
|
|
proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
|
|
parts: label_parts.into_iter().map(|label_part| {
|
|
let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string());
|
|
let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column });
|
|
let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column });
|
|
proto::InlayHintLabelPart {
|
|
value: label_part.value,
|
|
tooltip: label_part.tooltip.map(|tooltip| {
|
|
let proto_tooltip = match tooltip {
|
|
InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
|
|
InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
|
|
is_markdown: markup_content.kind == HoverBlockKind::Markdown,
|
|
value: markup_content.value,
|
|
}),
|
|
};
|
|
proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
|
|
}),
|
|
location_url,
|
|
location_range_start,
|
|
location_range_end,
|
|
language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64),
|
|
}}).collect()
|
|
})
|
|
}
|
|
}),
|
|
}),
|
|
kind: response_hint.kind.map(|kind| kind.name().to_string()),
|
|
tooltip: response_hint.tooltip.map(|response_tooltip| {
|
|
let proto_tooltip = match response_tooltip {
|
|
InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s),
|
|
InlayHintTooltip::MarkupContent(markup_content) => {
|
|
proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent {
|
|
is_markdown: markup_content.kind == HoverBlockKind::Markdown,
|
|
value: markup_content.value,
|
|
})
|
|
}
|
|
};
|
|
proto::InlayHintTooltip {
|
|
content: Some(proto_tooltip),
|
|
}
|
|
}),
|
|
resolve_state,
|
|
}
|
|
}
|
|
|
|
pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result<InlayHint> {
|
|
let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| {
|
|
panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",)
|
|
});
|
|
let resolve_state_data = resolve_state
|
|
.lsp_resolve_state.as_ref()
|
|
.map(|lsp_resolve_state| {
|
|
let value = lsp_resolve_state.value.as_deref().map(|value| {
|
|
serde_json::from_str::<Option<lsp::LSPAny>>(value)
|
|
.with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}"))
|
|
}).transpose()?.flatten();
|
|
anyhow::Ok((LanguageServerId(lsp_resolve_state.server_id as usize), value))
|
|
})
|
|
.transpose()?;
|
|
let resolve_state = match resolve_state.state {
|
|
0 => ResolveState::Resolved,
|
|
1 => {
|
|
let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| {
|
|
format!(
|
|
"No lsp resolve data for the hint that can be resolved: {message_hint:?}"
|
|
)
|
|
})?;
|
|
ResolveState::CanResolve(server_id, lsp_resolve_state)
|
|
}
|
|
2 => ResolveState::Resolving,
|
|
invalid => {
|
|
anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}")
|
|
}
|
|
};
|
|
Ok(InlayHint {
|
|
position: message_hint
|
|
.position
|
|
.and_then(language::proto::deserialize_anchor)
|
|
.context("invalid position")?,
|
|
label: match message_hint
|
|
.label
|
|
.and_then(|label| label.label)
|
|
.context("missing label")?
|
|
{
|
|
proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
|
|
proto::inlay_hint_label::Label::LabelParts(parts) => {
|
|
let mut label_parts = Vec::new();
|
|
for part in parts.parts {
|
|
label_parts.push(InlayHintLabelPart {
|
|
value: part.value,
|
|
tooltip: part.tooltip.map(|tooltip| match tooltip.content {
|
|
Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => {
|
|
InlayHintLabelPartTooltip::String(s)
|
|
}
|
|
Some(
|
|
proto::inlay_hint_label_part_tooltip::Content::MarkupContent(
|
|
markup_content,
|
|
),
|
|
) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
|
|
kind: if markup_content.is_markdown {
|
|
HoverBlockKind::Markdown
|
|
} else {
|
|
HoverBlockKind::PlainText
|
|
},
|
|
value: markup_content.value,
|
|
}),
|
|
None => InlayHintLabelPartTooltip::String(String::new()),
|
|
}),
|
|
location: {
|
|
match part
|
|
.location_url
|
|
.zip(
|
|
part.location_range_start.and_then(|start| {
|
|
Some(start..part.location_range_end?)
|
|
}),
|
|
)
|
|
.zip(part.language_server_id)
|
|
{
|
|
Some(((uri, range), server_id)) => Some((
|
|
LanguageServerId(server_id as usize),
|
|
lsp::Location {
|
|
uri: lsp::Url::parse(&uri)
|
|
.context("invalid uri in hint part {part:?}")?,
|
|
range: lsp::Range::new(
|
|
point_to_lsp(PointUtf16::new(
|
|
range.start.row,
|
|
range.start.column,
|
|
)),
|
|
point_to_lsp(PointUtf16::new(
|
|
range.end.row,
|
|
range.end.column,
|
|
)),
|
|
),
|
|
},
|
|
)),
|
|
None => None,
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
InlayHintLabel::LabelParts(label_parts)
|
|
}
|
|
},
|
|
padding_left: message_hint.padding_left,
|
|
padding_right: message_hint.padding_right,
|
|
kind: message_hint
|
|
.kind
|
|
.as_deref()
|
|
.and_then(InlayHintKind::from_name),
|
|
tooltip: message_hint.tooltip.and_then(|tooltip| {
|
|
Some(match tooltip.content? {
|
|
proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
|
|
proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
|
|
InlayHintTooltip::MarkupContent(MarkupContent {
|
|
kind: if markup_content.is_markdown {
|
|
HoverBlockKind::Markdown
|
|
} else {
|
|
HoverBlockKind::PlainText
|
|
},
|
|
value: markup_content.value,
|
|
})
|
|
}
|
|
})
|
|
}),
|
|
resolve_state,
|
|
})
|
|
}
|
|
|
|
pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint {
|
|
lsp::InlayHint {
|
|
position: point_to_lsp(hint.position.to_point_utf16(snapshot)),
|
|
kind: hint.kind.map(|kind| match kind {
|
|
InlayHintKind::Type => lsp::InlayHintKind::TYPE,
|
|
InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER,
|
|
}),
|
|
text_edits: None,
|
|
tooltip: hint.tooltip.and_then(|tooltip| {
|
|
Some(match tooltip {
|
|
InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s),
|
|
InlayHintTooltip::MarkupContent(markup_content) => {
|
|
lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent {
|
|
kind: match markup_content.kind {
|
|
HoverBlockKind::PlainText => lsp::MarkupKind::PlainText,
|
|
HoverBlockKind::Markdown => lsp::MarkupKind::Markdown,
|
|
HoverBlockKind::Code { .. } => return None,
|
|
},
|
|
value: markup_content.value,
|
|
})
|
|
}
|
|
})
|
|
}),
|
|
label: match hint.label {
|
|
InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s),
|
|
InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts(
|
|
label_parts
|
|
.into_iter()
|
|
.map(|part| lsp::InlayHintLabelPart {
|
|
value: part.value,
|
|
tooltip: part.tooltip.and_then(|tooltip| {
|
|
Some(match tooltip {
|
|
InlayHintLabelPartTooltip::String(s) => {
|
|
lsp::InlayHintLabelPartTooltip::String(s)
|
|
}
|
|
InlayHintLabelPartTooltip::MarkupContent(markup_content) => {
|
|
lsp::InlayHintLabelPartTooltip::MarkupContent(
|
|
lsp::MarkupContent {
|
|
kind: match markup_content.kind {
|
|
HoverBlockKind::PlainText => {
|
|
lsp::MarkupKind::PlainText
|
|
}
|
|
HoverBlockKind::Markdown => {
|
|
lsp::MarkupKind::Markdown
|
|
}
|
|
HoverBlockKind::Code { .. } => return None,
|
|
},
|
|
value: markup_content.value,
|
|
},
|
|
)
|
|
}
|
|
})
|
|
}),
|
|
location: part.location.map(|(_, location)| location),
|
|
command: None,
|
|
})
|
|
.collect(),
|
|
),
|
|
},
|
|
padding_left: Some(hint.padding_left),
|
|
padding_right: Some(hint.padding_right),
|
|
data: match hint.resolve_state {
|
|
ResolveState::CanResolve(_, data) => data,
|
|
ResolveState::Resolving | ResolveState::Resolved => None,
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool {
|
|
capabilities
|
|
.inlay_hint_provider
|
|
.as_ref()
|
|
.and_then(|options| match options {
|
|
OneOf::Left(_is_supported) => None,
|
|
OneOf::Right(capabilities) => match capabilities {
|
|
lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider,
|
|
lsp::InlayHintServerCapabilities::RegistrationOptions(o) => {
|
|
o.inlay_hint_options.resolve_provider
|
|
}
|
|
},
|
|
})
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
pub fn check_capabilities(capabilities: &ServerCapabilities) -> bool {
|
|
capabilities
|
|
.inlay_hint_provider
|
|
.as_ref()
|
|
.is_some_and(|inlay_hint_provider| match inlay_hint_provider {
|
|
lsp::OneOf::Left(enabled) => *enabled,
|
|
lsp::OneOf::Right(_) => true,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for InlayHints {
|
|
type Response = Vec<InlayHint>;
|
|
type LspRequest = lsp::InlayHintRequest;
|
|
type ProtoRequest = proto::InlayHints;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Inlay hints"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
Self::check_capabilities(&capabilities.server_capabilities)
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
buffer: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::InlayHintParams> {
|
|
Ok(lsp::InlayHintParams {
|
|
text_document: lsp::TextDocumentIdentifier {
|
|
uri: file_path_to_lsp_url(path)?,
|
|
},
|
|
range: range_to_lsp(self.range.to_point_utf16(buffer))?,
|
|
work_done_progress_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<Vec<lsp::InlayHint>>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
mut cx: AsyncApp,
|
|
) -> anyhow::Result<Vec<InlayHint>> {
|
|
let (lsp_adapter, lsp_server) =
|
|
language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?;
|
|
// `typescript-language-server` adds padding to the left for type hints, turning
|
|
// `const foo: boolean` into `const foo : boolean` which looks odd.
|
|
// `rust-analyzer` does not have the padding for this case, and we have to accommodate both.
|
|
//
|
|
// We could trim the whole string, but being pessimistic on par with the situation above,
|
|
// there might be a hint with multiple whitespaces at the end(s) which we need to display properly.
|
|
// Hence let's use a heuristic first to handle the most awkward case and look for more.
|
|
let force_no_type_left_padding =
|
|
lsp_adapter.name.0.as_ref() == "typescript-language-server";
|
|
|
|
let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| {
|
|
let resolve_state = if InlayHints::can_resolve_inlays(&lsp_server.capabilities()) {
|
|
ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone())
|
|
} else {
|
|
ResolveState::Resolved
|
|
};
|
|
|
|
let buffer = buffer.clone();
|
|
cx.spawn(async move |cx| {
|
|
InlayHints::lsp_to_project_hint(
|
|
lsp_hint,
|
|
&buffer,
|
|
server_id,
|
|
resolve_state,
|
|
force_no_type_left_padding,
|
|
cx,
|
|
)
|
|
.await
|
|
})
|
|
});
|
|
future::join_all(hints)
|
|
.await
|
|
.into_iter()
|
|
.collect::<anyhow::Result<_>>()
|
|
.context("lsp to project inlay hints conversion")
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
|
|
proto::InlayHints {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
start: Some(language::proto::serialize_anchor(&self.range.start)),
|
|
end: Some(language::proto::serialize_anchor(&self.range.end)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::InlayHints,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let start = message
|
|
.start
|
|
.and_then(language::proto::deserialize_anchor)
|
|
.context("invalid start")?;
|
|
let end = message
|
|
.end
|
|
.and_then(language::proto::deserialize_anchor)
|
|
.context("invalid end")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
|
|
Ok(Self { range: start..end })
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<InlayHint>,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
buffer_version: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::InlayHintsResponse {
|
|
proto::InlayHintsResponse {
|
|
hints: response
|
|
.into_iter()
|
|
.map(InlayHints::project_to_proto_hint)
|
|
.collect(),
|
|
version: serialize_version(buffer_version),
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::InlayHintsResponse,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> anyhow::Result<Vec<InlayHint>> {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
|
|
let mut hints = Vec::new();
|
|
for message_hint in message.hints {
|
|
hints.push(InlayHints::proto_to_project_hint(message_hint)?);
|
|
}
|
|
|
|
Ok(hints)
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::InlayHints) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetCodeLens {
|
|
type Response = Vec<CodeAction>;
|
|
type LspRequest = lsp::CodeLensRequest;
|
|
type ProtoRequest = proto::GetCodeLens;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Code Lens"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
capabilities
|
|
.server_capabilities
|
|
.code_lens_provider
|
|
.is_some()
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::CodeLensParams> {
|
|
Ok(lsp::CodeLensParams {
|
|
text_document: lsp::TextDocumentIdentifier {
|
|
uri: file_path_to_lsp_url(path)?,
|
|
},
|
|
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
|
|
partial_result_params: lsp::PartialResultParams::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<Vec<lsp::CodeLens>>,
|
|
lsp_store: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> anyhow::Result<Vec<CodeAction>> {
|
|
let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?;
|
|
let language_server = cx.update(|cx| {
|
|
lsp_store
|
|
.read(cx)
|
|
.language_server_for_id(server_id)
|
|
.with_context(|| {
|
|
format!("Missing the language server that just returned a response {server_id}")
|
|
})
|
|
})??;
|
|
let server_capabilities = language_server.capabilities();
|
|
let available_commands = server_capabilities
|
|
.execute_command_provider
|
|
.as_ref()
|
|
.map(|options| options.commands.as_slice())
|
|
.unwrap_or_default();
|
|
Ok(message
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.filter(|code_lens| {
|
|
code_lens
|
|
.command
|
|
.as_ref()
|
|
.is_none_or(|command| available_commands.contains(&command.command))
|
|
})
|
|
.map(|code_lens| {
|
|
let code_lens_range = range_from_lsp(code_lens.range);
|
|
let start = snapshot.clip_point_utf16(code_lens_range.start, Bias::Left);
|
|
let end = snapshot.clip_point_utf16(code_lens_range.end, Bias::Right);
|
|
let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
|
|
CodeAction {
|
|
server_id,
|
|
range,
|
|
lsp_action: LspAction::CodeLens(code_lens),
|
|
resolved: false,
|
|
}
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeLens {
|
|
proto::GetCodeLens {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::GetCodeLens,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
Ok(Self)
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<CodeAction>,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
buffer_version: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::GetCodeLensResponse {
|
|
proto::GetCodeLensResponse {
|
|
lens_actions: response
|
|
.iter()
|
|
.map(LspStore::serialize_code_action)
|
|
.collect(),
|
|
version: serialize_version(buffer_version),
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetCodeLensResponse,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> anyhow::Result<Vec<CodeAction>> {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
message
|
|
.lens_actions
|
|
.into_iter()
|
|
.map(LspStore::deserialize_code_action)
|
|
.collect::<Result<Vec<_>>>()
|
|
.context("deserializing proto code lens response")
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetCodeLens) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
impl LinkedEditingRange {
|
|
pub fn check_server_capabilities(capabilities: ServerCapabilities) -> bool {
|
|
let Some(linked_editing_options) = capabilities.linked_editing_range_provider else {
|
|
return false;
|
|
};
|
|
if let LinkedEditingRangeServerCapabilities::Simple(false) = linked_editing_options {
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for LinkedEditingRange {
|
|
type Response = Vec<Range<Anchor>>;
|
|
type LspRequest = lsp::request::LinkedEditingRange;
|
|
type ProtoRequest = proto::LinkedEditingRange;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Linked editing range"
|
|
}
|
|
|
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
|
Self::check_server_capabilities(capabilities.server_capabilities)
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
buffer: &Buffer,
|
|
_server: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::LinkedEditingRangeParams> {
|
|
let position = self.position.to_point_utf16(&buffer.snapshot());
|
|
Ok(lsp::LinkedEditingRangeParams {
|
|
text_document_position_params: make_lsp_text_document_position(path, position)?,
|
|
work_done_progress_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Option<lsp::LinkedEditingRanges>,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
_server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Vec<Range<Anchor>>> {
|
|
if let Some(lsp::LinkedEditingRanges { mut ranges, .. }) = message {
|
|
ranges.sort_by_key(|range| range.start);
|
|
|
|
buffer.read_with(&cx, |buffer, _| {
|
|
ranges
|
|
.into_iter()
|
|
.map(|range| {
|
|
let start =
|
|
buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left);
|
|
let end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left);
|
|
buffer.anchor_before(start)..buffer.anchor_after(end)
|
|
})
|
|
.collect()
|
|
})
|
|
} else {
|
|
Ok(vec![])
|
|
}
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LinkedEditingRange {
|
|
proto::LinkedEditingRange {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().to_proto(),
|
|
position: Some(serialize_anchor(&self.position)),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
message: proto::LinkedEditingRange,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Self> {
|
|
let position = message.position.context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
let position = deserialize_anchor(position).context("invalid position")?;
|
|
buffer
|
|
.update(&mut cx, |buffer, _| buffer.wait_for_anchors([position]))?
|
|
.await?;
|
|
Ok(Self { position })
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Vec<Range<Anchor>>,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
buffer_version: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::LinkedEditingRangeResponse {
|
|
proto::LinkedEditingRangeResponse {
|
|
items: response
|
|
.into_iter()
|
|
.map(|range| proto::AnchorRange {
|
|
start: Some(serialize_anchor(&range.start)),
|
|
end: Some(serialize_anchor(&range.end)),
|
|
})
|
|
.collect(),
|
|
version: serialize_version(buffer_version),
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::LinkedEditingRangeResponse,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<Vec<Range<Anchor>>> {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_version(deserialize_version(&message.version))
|
|
})?
|
|
.await?;
|
|
let items: Vec<Range<Anchor>> = message
|
|
.items
|
|
.into_iter()
|
|
.filter_map(|range| {
|
|
let start = deserialize_anchor(range.start?)?;
|
|
let end = deserialize_anchor(range.end?)?;
|
|
Some(start..end)
|
|
})
|
|
.collect();
|
|
for range in &items {
|
|
buffer
|
|
.update(&mut cx, |buffer, _| {
|
|
buffer.wait_for_anchors([range.start, range.end])
|
|
})?
|
|
.await?;
|
|
}
|
|
Ok(items)
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::LinkedEditingRange) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
impl GetDocumentDiagnostics {
|
|
pub fn diagnostics_from_proto(
|
|
response: proto::GetDocumentDiagnosticsResponse,
|
|
) -> Vec<LspPullDiagnostics> {
|
|
response
|
|
.pulled_diagnostics
|
|
.into_iter()
|
|
.filter_map(|diagnostics| {
|
|
Some(LspPullDiagnostics::Response {
|
|
server_id: LanguageServerId::from_proto(diagnostics.server_id),
|
|
uri: lsp::Url::from_str(diagnostics.uri.as_str()).log_err()?,
|
|
diagnostics: if diagnostics.changed {
|
|
PulledDiagnostics::Unchanged {
|
|
result_id: diagnostics.result_id?,
|
|
}
|
|
} else {
|
|
PulledDiagnostics::Changed {
|
|
result_id: diagnostics.result_id,
|
|
diagnostics: diagnostics
|
|
.diagnostics
|
|
.into_iter()
|
|
.filter_map(|diagnostic| {
|
|
GetDocumentDiagnostics::deserialize_lsp_diagnostic(diagnostic)
|
|
.context("deserializing diagnostics")
|
|
.log_err()
|
|
})
|
|
.collect(),
|
|
}
|
|
},
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result<lsp::Diagnostic> {
|
|
let start = diagnostic.start.context("invalid start range")?;
|
|
let end = diagnostic.end.context("invalid end range")?;
|
|
|
|
let range = Range::<PointUtf16> {
|
|
start: PointUtf16 {
|
|
row: start.row,
|
|
column: start.column,
|
|
},
|
|
end: PointUtf16 {
|
|
row: end.row,
|
|
column: end.column,
|
|
},
|
|
};
|
|
|
|
let data = diagnostic.data.and_then(|data| Value::from_str(&data).ok());
|
|
let code = diagnostic.code.map(lsp::NumberOrString::String);
|
|
|
|
let related_information = diagnostic
|
|
.related_information
|
|
.into_iter()
|
|
.map(|info| {
|
|
let start = info.location_range_start.unwrap();
|
|
let end = info.location_range_end.unwrap();
|
|
|
|
lsp::DiagnosticRelatedInformation {
|
|
location: lsp::Location {
|
|
range: lsp::Range {
|
|
start: point_to_lsp(PointUtf16::new(start.row, start.column)),
|
|
end: point_to_lsp(PointUtf16::new(end.row, end.column)),
|
|
},
|
|
uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(),
|
|
},
|
|
message: info.message,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let tags = diagnostic
|
|
.tags
|
|
.into_iter()
|
|
.filter_map(|tag| match proto::LspDiagnosticTag::from_i32(tag) {
|
|
Some(proto::LspDiagnosticTag::Unnecessary) => Some(lsp::DiagnosticTag::UNNECESSARY),
|
|
Some(proto::LspDiagnosticTag::Deprecated) => Some(lsp::DiagnosticTag::DEPRECATED),
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
Ok(lsp::Diagnostic {
|
|
range: language::range_to_lsp(range)?,
|
|
severity: match proto::lsp_diagnostic::Severity::from_i32(diagnostic.severity).unwrap()
|
|
{
|
|
proto::lsp_diagnostic::Severity::Error => Some(lsp::DiagnosticSeverity::ERROR),
|
|
proto::lsp_diagnostic::Severity::Warning => Some(lsp::DiagnosticSeverity::WARNING),
|
|
proto::lsp_diagnostic::Severity::Information => {
|
|
Some(lsp::DiagnosticSeverity::INFORMATION)
|
|
}
|
|
proto::lsp_diagnostic::Severity::Hint => Some(lsp::DiagnosticSeverity::HINT),
|
|
_ => None,
|
|
},
|
|
code,
|
|
code_description: diagnostic
|
|
.code_description
|
|
.map(|code_description| CodeDescription {
|
|
href: Some(lsp::Url::parse(&code_description).unwrap()),
|
|
}),
|
|
related_information: Some(related_information),
|
|
tags: Some(tags),
|
|
source: diagnostic.source.clone(),
|
|
message: diagnostic.message,
|
|
data,
|
|
})
|
|
}
|
|
|
|
fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result<proto::LspDiagnostic> {
|
|
let range = language::range_from_lsp(diagnostic.range);
|
|
let related_information = diagnostic
|
|
.related_information
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.map(|related_information| {
|
|
let location_range_start =
|
|
point_from_lsp(related_information.location.range.start).0;
|
|
let location_range_end = point_from_lsp(related_information.location.range.end).0;
|
|
|
|
Ok(proto::LspDiagnosticRelatedInformation {
|
|
location_url: Some(related_information.location.uri.to_string()),
|
|
location_range_start: Some(proto::PointUtf16 {
|
|
row: location_range_start.row,
|
|
column: location_range_start.column,
|
|
}),
|
|
location_range_end: Some(proto::PointUtf16 {
|
|
row: location_range_end.row,
|
|
column: location_range_end.column,
|
|
}),
|
|
message: related_information.message,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>>>()?;
|
|
|
|
let tags = diagnostic
|
|
.tags
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.map(|tag| match tag {
|
|
lsp::DiagnosticTag::UNNECESSARY => proto::LspDiagnosticTag::Unnecessary,
|
|
lsp::DiagnosticTag::DEPRECATED => proto::LspDiagnosticTag::Deprecated,
|
|
_ => proto::LspDiagnosticTag::None,
|
|
} as i32)
|
|
.collect();
|
|
|
|
Ok(proto::LspDiagnostic {
|
|
start: Some(proto::PointUtf16 {
|
|
row: range.start.0.row,
|
|
column: range.start.0.column,
|
|
}),
|
|
end: Some(proto::PointUtf16 {
|
|
row: range.end.0.row,
|
|
column: range.end.0.column,
|
|
}),
|
|
severity: match diagnostic.severity {
|
|
Some(lsp::DiagnosticSeverity::ERROR) => proto::lsp_diagnostic::Severity::Error,
|
|
Some(lsp::DiagnosticSeverity::WARNING) => proto::lsp_diagnostic::Severity::Warning,
|
|
Some(lsp::DiagnosticSeverity::INFORMATION) => {
|
|
proto::lsp_diagnostic::Severity::Information
|
|
}
|
|
Some(lsp::DiagnosticSeverity::HINT) => proto::lsp_diagnostic::Severity::Hint,
|
|
_ => proto::lsp_diagnostic::Severity::None,
|
|
} as i32,
|
|
code: diagnostic.code.as_ref().map(|code| match code {
|
|
lsp::NumberOrString::Number(code) => code.to_string(),
|
|
lsp::NumberOrString::String(code) => code.clone(),
|
|
}),
|
|
source: diagnostic.source.clone(),
|
|
related_information,
|
|
tags,
|
|
code_description: diagnostic
|
|
.code_description
|
|
.and_then(|desc| desc.href.map(|url| url.to_string())),
|
|
message: diagnostic.message,
|
|
data: diagnostic.data.as_ref().map(|data| data.to_string()),
|
|
})
|
|
}
|
|
|
|
pub fn deserialize_workspace_diagnostics_report(
|
|
report: lsp::WorkspaceDiagnosticReportResult,
|
|
server_id: LanguageServerId,
|
|
) -> Vec<WorkspaceLspPullDiagnostics> {
|
|
let mut pulled_diagnostics = HashMap::default();
|
|
match report {
|
|
lsp::WorkspaceDiagnosticReportResult::Report(workspace_diagnostic_report) => {
|
|
for report in workspace_diagnostic_report.items {
|
|
match report {
|
|
lsp::WorkspaceDocumentDiagnosticReport::Full(report) => {
|
|
process_full_workspace_diagnostics_report(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
report,
|
|
)
|
|
}
|
|
lsp::WorkspaceDocumentDiagnosticReport::Unchanged(report) => {
|
|
process_unchanged_workspace_diagnostics_report(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
report,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
lsp::WorkspaceDiagnosticReportResult::Partial(
|
|
workspace_diagnostic_report_partial_result,
|
|
) => {
|
|
for report in workspace_diagnostic_report_partial_result.items {
|
|
match report {
|
|
lsp::WorkspaceDocumentDiagnosticReport::Full(report) => {
|
|
process_full_workspace_diagnostics_report(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
report,
|
|
)
|
|
}
|
|
lsp::WorkspaceDocumentDiagnosticReport::Unchanged(report) => {
|
|
process_unchanged_workspace_diagnostics_report(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
report,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pulled_diagnostics.into_values().collect()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct WorkspaceLspPullDiagnostics {
|
|
pub version: Option<i32>,
|
|
pub diagnostics: LspPullDiagnostics,
|
|
}
|
|
|
|
fn process_full_workspace_diagnostics_report(
|
|
diagnostics: &mut HashMap<lsp::Url, WorkspaceLspPullDiagnostics>,
|
|
server_id: LanguageServerId,
|
|
report: lsp::WorkspaceFullDocumentDiagnosticReport,
|
|
) {
|
|
let mut new_diagnostics = HashMap::default();
|
|
process_full_diagnostics_report(
|
|
&mut new_diagnostics,
|
|
server_id,
|
|
report.uri,
|
|
report.full_document_diagnostic_report,
|
|
);
|
|
diagnostics.extend(new_diagnostics.into_iter().map(|(uri, diagnostics)| {
|
|
(
|
|
uri,
|
|
WorkspaceLspPullDiagnostics {
|
|
version: report.version.map(|v| v as i32),
|
|
diagnostics,
|
|
},
|
|
)
|
|
}));
|
|
}
|
|
|
|
fn process_unchanged_workspace_diagnostics_report(
|
|
diagnostics: &mut HashMap<lsp::Url, WorkspaceLspPullDiagnostics>,
|
|
server_id: LanguageServerId,
|
|
report: lsp::WorkspaceUnchangedDocumentDiagnosticReport,
|
|
) {
|
|
let mut new_diagnostics = HashMap::default();
|
|
process_unchanged_diagnostics_report(
|
|
&mut new_diagnostics,
|
|
server_id,
|
|
report.uri,
|
|
report.unchanged_document_diagnostic_report,
|
|
);
|
|
diagnostics.extend(new_diagnostics.into_iter().map(|(uri, diagnostics)| {
|
|
(
|
|
uri,
|
|
WorkspaceLspPullDiagnostics {
|
|
version: report.version.map(|v| v as i32),
|
|
diagnostics,
|
|
},
|
|
)
|
|
}));
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetDocumentDiagnostics {
|
|
type Response = Vec<LspPullDiagnostics>;
|
|
type LspRequest = lsp::request::DocumentDiagnosticRequest;
|
|
type ProtoRequest = proto::GetDocumentDiagnostics;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Get diagnostics"
|
|
}
|
|
|
|
fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
|
|
server_capabilities
|
|
.server_capabilities
|
|
.diagnostic_provider
|
|
.is_some()
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
language_server: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::DocumentDiagnosticParams> {
|
|
let identifier = match language_server.capabilities().diagnostic_provider {
|
|
Some(lsp::DiagnosticServerCapabilities::Options(options)) => options.identifier,
|
|
Some(lsp::DiagnosticServerCapabilities::RegistrationOptions(options)) => {
|
|
options.diagnostic_options.identifier
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
Ok(lsp::DocumentDiagnosticParams {
|
|
text_document: lsp::TextDocumentIdentifier {
|
|
uri: file_path_to_lsp_url(path)?,
|
|
},
|
|
identifier,
|
|
previous_result_id: self.previous_result_id.clone(),
|
|
partial_result_params: Default::default(),
|
|
work_done_progress_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: lsp::DocumentDiagnosticReportResult,
|
|
_: Entity<LspStore>,
|
|
buffer: Entity<Buffer>,
|
|
server_id: LanguageServerId,
|
|
cx: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
let url = buffer.read_with(&cx, |buffer, cx| {
|
|
buffer
|
|
.file()
|
|
.and_then(|file| file.as_local())
|
|
.map(|file| {
|
|
let abs_path = file.abs_path(cx);
|
|
file_path_to_lsp_url(&abs_path)
|
|
})
|
|
.transpose()?
|
|
.with_context(|| format!("missing url on buffer {}", buffer.remote_id()))
|
|
})??;
|
|
|
|
let mut pulled_diagnostics = HashMap::default();
|
|
match message {
|
|
lsp::DocumentDiagnosticReportResult::Report(report) => match report {
|
|
lsp::DocumentDiagnosticReport::Full(report) => {
|
|
if let Some(related_documents) = report.related_documents {
|
|
process_related_documents(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
related_documents,
|
|
);
|
|
}
|
|
process_full_diagnostics_report(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
url,
|
|
report.full_document_diagnostic_report,
|
|
);
|
|
}
|
|
lsp::DocumentDiagnosticReport::Unchanged(report) => {
|
|
if let Some(related_documents) = report.related_documents {
|
|
process_related_documents(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
related_documents,
|
|
);
|
|
}
|
|
process_unchanged_diagnostics_report(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
url,
|
|
report.unchanged_document_diagnostic_report,
|
|
);
|
|
}
|
|
},
|
|
lsp::DocumentDiagnosticReportResult::Partial(report) => {
|
|
if let Some(related_documents) = report.related_documents {
|
|
process_related_documents(
|
|
&mut pulled_diagnostics,
|
|
server_id,
|
|
related_documents,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(pulled_diagnostics.into_values().collect())
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentDiagnostics {
|
|
proto::GetDocumentDiagnostics {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().into(),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
_: proto::GetDocumentDiagnostics,
|
|
_: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: AsyncApp,
|
|
) -> Result<Self> {
|
|
anyhow::bail!(
|
|
"proto::GetDocumentDiagnostics is not expected to be converted from proto directly, as it needs `previous_result_id` fetched first"
|
|
)
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Self::Response,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
_: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::GetDocumentDiagnosticsResponse {
|
|
let pulled_diagnostics = response
|
|
.into_iter()
|
|
.filter_map(|diagnostics| match diagnostics {
|
|
LspPullDiagnostics::Default => None,
|
|
LspPullDiagnostics::Response {
|
|
server_id,
|
|
uri,
|
|
diagnostics,
|
|
} => {
|
|
let mut changed = false;
|
|
let (diagnostics, result_id) = match diagnostics {
|
|
PulledDiagnostics::Unchanged { result_id } => (Vec::new(), Some(result_id)),
|
|
PulledDiagnostics::Changed {
|
|
result_id,
|
|
diagnostics,
|
|
} => {
|
|
changed = true;
|
|
(diagnostics, result_id)
|
|
}
|
|
};
|
|
Some(proto::PulledDiagnostics {
|
|
changed,
|
|
result_id,
|
|
uri: uri.to_string(),
|
|
server_id: server_id.to_proto(),
|
|
diagnostics: diagnostics
|
|
.into_iter()
|
|
.filter_map(|diagnostic| {
|
|
GetDocumentDiagnostics::serialize_lsp_diagnostic(diagnostic)
|
|
.context("serializing diagnostics")
|
|
.log_err()
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
proto::GetDocumentDiagnosticsResponse { pulled_diagnostics }
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
response: proto::GetDocumentDiagnosticsResponse,
|
|
_: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
Ok(Self::diagnostics_from_proto(response))
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &proto::GetDocumentDiagnostics) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl LspCommand for GetDocumentColor {
|
|
type Response = Vec<DocumentColor>;
|
|
type LspRequest = lsp::request::DocumentColor;
|
|
type ProtoRequest = proto::GetDocumentColor;
|
|
|
|
fn display_name(&self) -> &str {
|
|
"Document color"
|
|
}
|
|
|
|
fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool {
|
|
server_capabilities
|
|
.server_capabilities
|
|
.color_provider
|
|
.as_ref()
|
|
.is_some_and(|capability| match capability {
|
|
lsp::ColorProviderCapability::Simple(supported) => *supported,
|
|
lsp::ColorProviderCapability::ColorProvider(..) => true,
|
|
lsp::ColorProviderCapability::Options(..) => true,
|
|
})
|
|
}
|
|
|
|
fn to_lsp(
|
|
&self,
|
|
path: &Path,
|
|
_: &Buffer,
|
|
_: &Arc<LanguageServer>,
|
|
_: &App,
|
|
) -> Result<lsp::DocumentColorParams> {
|
|
Ok(lsp::DocumentColorParams {
|
|
text_document: make_text_document_identifier(path)?,
|
|
work_done_progress_params: Default::default(),
|
|
partial_result_params: Default::default(),
|
|
})
|
|
}
|
|
|
|
async fn response_from_lsp(
|
|
self,
|
|
message: Vec<lsp::ColorInformation>,
|
|
_: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: LanguageServerId,
|
|
_: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
Ok(message
|
|
.into_iter()
|
|
.map(|color| DocumentColor {
|
|
lsp_range: color.range,
|
|
color: color.color,
|
|
resolved: false,
|
|
color_presentations: Vec::new(),
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
|
|
proto::GetDocumentColor {
|
|
project_id,
|
|
buffer_id: buffer.remote_id().to_proto(),
|
|
version: serialize_version(&buffer.version()),
|
|
}
|
|
}
|
|
|
|
async fn from_proto(
|
|
_: Self::ProtoRequest,
|
|
_: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: AsyncApp,
|
|
) -> Result<Self> {
|
|
Ok(Self {})
|
|
}
|
|
|
|
fn response_to_proto(
|
|
response: Self::Response,
|
|
_: &mut LspStore,
|
|
_: PeerId,
|
|
buffer_version: &clock::Global,
|
|
_: &mut App,
|
|
) -> proto::GetDocumentColorResponse {
|
|
proto::GetDocumentColorResponse {
|
|
colors: response
|
|
.into_iter()
|
|
.map(|color| {
|
|
let start = point_from_lsp(color.lsp_range.start).0;
|
|
let end = point_from_lsp(color.lsp_range.end).0;
|
|
proto::ColorInformation {
|
|
red: color.color.red,
|
|
green: color.color.green,
|
|
blue: color.color.blue,
|
|
alpha: color.color.alpha,
|
|
lsp_range_start: Some(proto::PointUtf16 {
|
|
row: start.row,
|
|
column: start.column,
|
|
}),
|
|
lsp_range_end: Some(proto::PointUtf16 {
|
|
row: end.row,
|
|
column: end.column,
|
|
}),
|
|
}
|
|
})
|
|
.collect(),
|
|
version: serialize_version(buffer_version),
|
|
}
|
|
}
|
|
|
|
async fn response_from_proto(
|
|
self,
|
|
message: proto::GetDocumentColorResponse,
|
|
_: Entity<LspStore>,
|
|
_: Entity<Buffer>,
|
|
_: AsyncApp,
|
|
) -> Result<Self::Response> {
|
|
Ok(message
|
|
.colors
|
|
.into_iter()
|
|
.filter_map(|color| {
|
|
let start = color.lsp_range_start?;
|
|
let start = PointUtf16::new(start.row, start.column);
|
|
let end = color.lsp_range_end?;
|
|
let end = PointUtf16::new(end.row, end.column);
|
|
Some(DocumentColor {
|
|
resolved: false,
|
|
color_presentations: Vec::new(),
|
|
lsp_range: lsp::Range {
|
|
start: point_to_lsp(start),
|
|
end: point_to_lsp(end),
|
|
},
|
|
color: lsp::Color {
|
|
red: color.red,
|
|
green: color.green,
|
|
blue: color.blue,
|
|
alpha: color.alpha,
|
|
},
|
|
})
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {
|
|
BufferId::new(message.buffer_id)
|
|
}
|
|
}
|
|
|
|
fn process_related_documents(
|
|
diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
|
|
server_id: LanguageServerId,
|
|
documents: impl IntoIterator<Item = (lsp::Url, lsp::DocumentDiagnosticReportKind)>,
|
|
) {
|
|
for (url, report_kind) in documents {
|
|
match report_kind {
|
|
lsp::DocumentDiagnosticReportKind::Full(report) => {
|
|
process_full_diagnostics_report(diagnostics, server_id, url, report)
|
|
}
|
|
lsp::DocumentDiagnosticReportKind::Unchanged(report) => {
|
|
process_unchanged_diagnostics_report(diagnostics, server_id, url, report)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_unchanged_diagnostics_report(
|
|
diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
|
|
server_id: LanguageServerId,
|
|
uri: lsp::Url,
|
|
report: lsp::UnchangedDocumentDiagnosticReport,
|
|
) {
|
|
let result_id = report.result_id;
|
|
match diagnostics.entry(uri.clone()) {
|
|
hash_map::Entry::Occupied(mut o) => match o.get_mut() {
|
|
LspPullDiagnostics::Default => {
|
|
o.insert(LspPullDiagnostics::Response {
|
|
server_id,
|
|
uri,
|
|
diagnostics: PulledDiagnostics::Unchanged { result_id },
|
|
});
|
|
}
|
|
LspPullDiagnostics::Response {
|
|
server_id: existing_server_id,
|
|
uri: existing_uri,
|
|
diagnostics: existing_diagnostics,
|
|
} => {
|
|
if server_id != *existing_server_id || &uri != existing_uri {
|
|
debug_panic!(
|
|
"Unexpected state: file {uri} has two different sets of diagnostics reported"
|
|
);
|
|
}
|
|
match existing_diagnostics {
|
|
PulledDiagnostics::Unchanged { .. } => {
|
|
*existing_diagnostics = PulledDiagnostics::Unchanged { result_id };
|
|
}
|
|
PulledDiagnostics::Changed { .. } => {}
|
|
}
|
|
}
|
|
},
|
|
hash_map::Entry::Vacant(v) => {
|
|
v.insert(LspPullDiagnostics::Response {
|
|
server_id,
|
|
uri,
|
|
diagnostics: PulledDiagnostics::Unchanged { result_id },
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_full_diagnostics_report(
|
|
diagnostics: &mut HashMap<lsp::Url, LspPullDiagnostics>,
|
|
server_id: LanguageServerId,
|
|
uri: lsp::Url,
|
|
report: lsp::FullDocumentDiagnosticReport,
|
|
) {
|
|
let result_id = report.result_id;
|
|
match diagnostics.entry(uri.clone()) {
|
|
hash_map::Entry::Occupied(mut o) => match o.get_mut() {
|
|
LspPullDiagnostics::Default => {
|
|
o.insert(LspPullDiagnostics::Response {
|
|
server_id,
|
|
uri,
|
|
diagnostics: PulledDiagnostics::Changed {
|
|
result_id,
|
|
diagnostics: report.items,
|
|
},
|
|
});
|
|
}
|
|
LspPullDiagnostics::Response {
|
|
server_id: existing_server_id,
|
|
uri: existing_uri,
|
|
diagnostics: existing_diagnostics,
|
|
} => {
|
|
if server_id != *existing_server_id || &uri != existing_uri {
|
|
debug_panic!(
|
|
"Unexpected state: file {uri} has two different sets of diagnostics reported"
|
|
);
|
|
}
|
|
match existing_diagnostics {
|
|
PulledDiagnostics::Unchanged { .. } => {
|
|
*existing_diagnostics = PulledDiagnostics::Changed {
|
|
result_id,
|
|
diagnostics: report.items,
|
|
};
|
|
}
|
|
PulledDiagnostics::Changed {
|
|
result_id: existing_result_id,
|
|
diagnostics: existing_diagnostics,
|
|
} => {
|
|
if result_id.is_some() {
|
|
*existing_result_id = result_id;
|
|
}
|
|
existing_diagnostics.extend(report.items);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
hash_map::Entry::Vacant(v) => {
|
|
v.insert(LspPullDiagnostics::Response {
|
|
server_id,
|
|
uri,
|
|
diagnostics: PulledDiagnostics::Changed {
|
|
result_id,
|
|
diagnostics: report.items,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use lsp::{DiagnosticSeverity, DiagnosticTag};
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn test_serialize_lsp_diagnostic() {
|
|
let lsp_diagnostic = lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position::new(0, 1),
|
|
end: lsp::Position::new(2, 3),
|
|
},
|
|
severity: Some(DiagnosticSeverity::ERROR),
|
|
code: Some(lsp::NumberOrString::String("E001".to_string())),
|
|
source: Some("test-source".to_string()),
|
|
message: "Test error message".to_string(),
|
|
related_information: None,
|
|
tags: Some(vec![DiagnosticTag::DEPRECATED]),
|
|
code_description: None,
|
|
data: Some(json!({"detail": "test detail"})),
|
|
};
|
|
|
|
let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic)
|
|
.expect("Failed to serialize diagnostic");
|
|
|
|
let start = proto_diagnostic.start.unwrap();
|
|
let end = proto_diagnostic.end.unwrap();
|
|
assert_eq!(start.row, 0);
|
|
assert_eq!(start.column, 1);
|
|
assert_eq!(end.row, 2);
|
|
assert_eq!(end.column, 3);
|
|
assert_eq!(
|
|
proto_diagnostic.severity,
|
|
proto::lsp_diagnostic::Severity::Error as i32
|
|
);
|
|
assert_eq!(proto_diagnostic.code, Some("E001".to_string()));
|
|
assert_eq!(proto_diagnostic.source, Some("test-source".to_string()));
|
|
assert_eq!(proto_diagnostic.message, "Test error message");
|
|
}
|
|
|
|
#[test]
|
|
fn test_deserialize_lsp_diagnostic() {
|
|
let proto_diagnostic = proto::LspDiagnostic {
|
|
start: Some(proto::PointUtf16 { row: 0, column: 1 }),
|
|
end: Some(proto::PointUtf16 { row: 2, column: 3 }),
|
|
severity: proto::lsp_diagnostic::Severity::Warning as i32,
|
|
code: Some("ERR".to_string()),
|
|
source: Some("Prism".to_string()),
|
|
message: "assigned but unused variable - a".to_string(),
|
|
related_information: vec![],
|
|
tags: vec![],
|
|
code_description: None,
|
|
data: None,
|
|
};
|
|
|
|
let lsp_diagnostic = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic)
|
|
.expect("Failed to deserialize diagnostic");
|
|
|
|
assert_eq!(lsp_diagnostic.range.start.line, 0);
|
|
assert_eq!(lsp_diagnostic.range.start.character, 1);
|
|
assert_eq!(lsp_diagnostic.range.end.line, 2);
|
|
assert_eq!(lsp_diagnostic.range.end.character, 3);
|
|
assert_eq!(lsp_diagnostic.severity, Some(DiagnosticSeverity::WARNING));
|
|
assert_eq!(
|
|
lsp_diagnostic.code,
|
|
Some(lsp::NumberOrString::String("ERR".to_string()))
|
|
);
|
|
assert_eq!(lsp_diagnostic.source, Some("Prism".to_string()));
|
|
assert_eq!(lsp_diagnostic.message, "assigned but unused variable - a");
|
|
}
|
|
|
|
#[test]
|
|
fn test_related_information() {
|
|
let related_info = lsp::DiagnosticRelatedInformation {
|
|
location: lsp::Location {
|
|
uri: lsp::Url::parse("file:///test.rs").unwrap(),
|
|
range: lsp::Range {
|
|
start: lsp::Position::new(1, 1),
|
|
end: lsp::Position::new(1, 5),
|
|
},
|
|
},
|
|
message: "Related info message".to_string(),
|
|
};
|
|
|
|
let lsp_diagnostic = lsp::Diagnostic {
|
|
range: lsp::Range {
|
|
start: lsp::Position::new(0, 0),
|
|
end: lsp::Position::new(0, 1),
|
|
},
|
|
severity: Some(DiagnosticSeverity::INFORMATION),
|
|
code: None,
|
|
source: Some("Prism".to_string()),
|
|
message: "assigned but unused variable - a".to_string(),
|
|
related_information: Some(vec![related_info]),
|
|
tags: None,
|
|
code_description: None,
|
|
data: None,
|
|
};
|
|
|
|
let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic)
|
|
.expect("Failed to serialize diagnostic");
|
|
|
|
assert_eq!(proto_diagnostic.related_information.len(), 1);
|
|
let related = &proto_diagnostic.related_information[0];
|
|
assert_eq!(related.location_url, Some("file:///test.rs".to_string()));
|
|
assert_eq!(related.message, "Related info message");
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_ranges() {
|
|
let proto_diagnostic = proto::LspDiagnostic {
|
|
start: None,
|
|
end: Some(proto::PointUtf16 { row: 2, column: 3 }),
|
|
severity: proto::lsp_diagnostic::Severity::Error as i32,
|
|
code: None,
|
|
source: None,
|
|
message: "Test message".to_string(),
|
|
related_information: vec![],
|
|
tags: vec![],
|
|
code_description: None,
|
|
data: None,
|
|
};
|
|
|
|
let result = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic);
|
|
assert!(result.is_err());
|
|
}
|
|
}
|