mod signature_help; use crate::{ CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, 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 { 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 { Ok(lsp::TextDocumentIdentifier { uri: file_path_to_lsp_url(path)?, }) } pub(crate) fn make_lsp_text_document_position( path: &Path, position: PointUtf16, ) -> Result { 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 { None } fn to_lsp_params_or_response( &self, path: &Path, buffer: &Buffer, language_server: &Arc, cx: &App, ) -> Result< LspParamsOrResponse<::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 { true } fn to_lsp( &self, path: &Path, buffer: &Buffer, language_server: &Arc, cx: &App, ) -> Result<::Params>; async fn response_from_lsp( self, message: ::Result, lsp_store: Entity, buffer: Entity, server_id: LanguageServerId, cx: AsyncApp, ) -> Result; fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest; async fn from_proto( message: Self::ProtoRequest, lsp_store: Entity, buffer: Entity, cx: AsyncApp, ) -> Result; fn response_to_proto( response: Self::Response, lsp_store: &mut LspStore, peer_id: PeerId, buffer_version: &clock::Global, cx: &mut App, ) -> ::Response; async fn response_from_proto( self, message: ::Response, lsp_store: Entity, buffer: Entity, cx: AsyncApp, ) -> Result; fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result; } pub enum LspParamsOrResponse { 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)] pub struct GetDefinition { pub position: PointUtf16, } #[derive(Debug)] pub(crate) struct GetDeclaration { pub position: PointUtf16, } #[derive(Debug)] pub(crate) struct GetTypeDefinition { pub position: PointUtf16, } #[derive(Debug)] pub(crate) struct GetImplementation { pub position: PointUtf16, } #[derive(Debug)] 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, pub kinds: Option>, } #[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, } #[derive(Debug, Copy, Clone)] pub(crate) struct GetCodeLens; 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 {} #[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 to_lsp_params_or_response( &self, path: &Path, buffer: &Buffer, language_server: &Arc, cx: &App, ) -> Result> { 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, _: &App, ) -> Result { make_lsp_text_document_position(path, self.position) } async fn response_from_lsp( self, message: Option, _: Entity, buffer: Entity, _: LanguageServerId, mut cx: AsyncApp, ) -> Result { buffer.read_with(&mut 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); 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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::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 to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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_store: Entity, buffer: Entity, server_id: LanguageServerId, mut cx: AsyncApp, ) -> Result { if let Some(edit) = message { let (lsp_adapter, 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_adapter, 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut 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, _: Entity, mut cx: AsyncApp, ) -> Result { 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::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetDefinition { type Response = Vec; 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() } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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_store: Entity, buffer: Entity, server_id: LanguageServerId, cx: AsyncApp, ) -> Result> { 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( response: Vec, 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, _: Entity, cx: AsyncApp, ) -> Result> { location_links_from_proto(message.links, lsp_store, cx).await } fn buffer_id_from_proto(message: &proto::GetDefinition) -> Result { BufferId::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetDeclaration { type Response = Vec; 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() } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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_store: Entity, buffer: Entity, server_id: LanguageServerId, cx: AsyncApp, ) -> Result> { 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( response: Vec, 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, _: Entity, cx: AsyncApp, ) -> Result> { location_links_from_proto(message.links, lsp_store, cx).await } fn buffer_id_from_proto(message: &proto::GetDeclaration) -> Result { BufferId::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetImplementation { type Response = Vec; type LspRequest = lsp::request::GotoImplementation; type ProtoRequest = proto::GetImplementation; fn display_name(&self) -> &str { "Get implementation" } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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_store: Entity, buffer: Entity, server_id: LanguageServerId, cx: AsyncApp, ) -> Result> { 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( response: Vec, 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, _: Entity, cx: AsyncApp, ) -> Result> { location_links_from_proto(message.links, project, cx).await } fn buffer_id_from_proto(message: &proto::GetImplementation) -> Result { BufferId::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetTypeDefinition { type Response = Vec; 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, _: &App, ) -> Result { 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, project: Entity, buffer: Entity, server_id: LanguageServerId, cx: AsyncApp, ) -> Result> { 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( response: Vec, 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, _: Entity, cx: AsyncApp, ) -> Result> { location_links_from_proto(message.links, project, cx).await } fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> Result { BufferId::new(message.buffer_id) } } fn language_server_for_buffer( lsp_store: &Entity, buffer: &Entity, server_id: LanguageServerId, cx: &mut AsyncApp, ) -> Result<(Arc, Arc)> { 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, lsp_store: Entity, mut cx: AsyncApp, ) -> Result> { 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, cx: &mut AsyncApp, ) -> Task> { 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_store: Entity, buffer: Entity, server_id: LanguageServerId, mut cx: AsyncApp, ) -> Result> { 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 (lsp_adapter, 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(), lsp_adapter.name.clone(), 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, buffer: &Entity, server_id: LanguageServerId, cx: &mut AsyncApp, ) -> Result { let (lsp_adapter, 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(), lsp_adapter.name.clone(), 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, lsp_store: &mut LspStore, peer_id: PeerId, cx: &mut App, ) -> Vec { 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; type LspRequest = lsp::request::References; type ProtoRequest = proto::GetReferences; fn display_name(&self) -> &str { "Find all references" } fn status(&self) -> Option { 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, _: &App, ) -> Result { 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>, lsp_store: Entity, buffer: Entity, server_id: LanguageServerId, mut cx: AsyncApp, ) -> Result> { let mut references = Vec::new(); let (lsp_adapter, 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(), lsp_adapter.name.clone(), cx, ) })? .await?; target_buffer_handle .clone() .read_with(&mut 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( response: Vec, 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, _: Entity, mut cx: AsyncApp, ) -> Result> { 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::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetDocumentHighlights { type Response = Vec; 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() } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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>, _: Entity, buffer: Entity, _: LanguageServerId, mut cx: AsyncApp, ) -> Result> { buffer.read_with(&mut 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( response: Vec, _: &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, buffer: Entity, mut cx: AsyncApp, ) -> Result> { 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::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetDocumentSymbols { type Response = Vec; 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() } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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, _: Entity, _: Entity, _: LanguageServerId, _: AsyncApp, ) -> Result> { 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::>() }) .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, buffer: Entity, mut cx: AsyncApp, ) -> Result { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) })? .await?; Ok(Self) } fn response_to_proto( response: Vec, _: &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::(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::>(); proto::GetDocumentSymbolsResponse { symbols } } async fn response_from_proto( self, message: proto::GetDocumentSymbolsResponse, _: Entity, _: Entity, _: AsyncApp, ) -> Result> { 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 { let kind = unsafe { mem::transmute::(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::>(), }) } symbols.push(deserialize_symbol_with_children(serialized_symbol)?); } Ok(symbols) } fn buffer_id_from_proto(message: &proto::GetDocumentSymbols) -> Result { BufferId::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetSignatureHelp { type Response = Option; 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, _cx: &App, ) -> Result { 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, _: Entity, _: Entity, _: LanguageServerId, _: AsyncApp, ) -> Result { Ok(message.and_then(SignatureHelp::new)) } 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut 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, _: Entity, _: Entity, _: AsyncApp, ) -> Result { Ok(response .signature_help .map(proto_to_lsp_signature) .and_then(SignatureHelp::new)) } fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result { BufferId::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetHover { type Response = Option; 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, _: &App, ) -> Result { 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, _: Entity, buffer: Entity, _: LanguageServerId, mut cx: AsyncApp, ) -> Result { let Some(hover) = message else { return Ok(None); }; let (language, range) = buffer.read_with(&mut 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 { 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut 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::new(message.buffer_id) } } #[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 to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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_store: Entity, buffer: Entity, server_id: LanguageServerId, mut cx: AsyncApp, ) -> Result { 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(&mut 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); 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() { if 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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::>>()?; Ok(CoreCompletionResponse { completions, is_incomplete: !message.can_reuse, }) } fn buffer_id_from_proto(message: &proto::GetCompletions) -> Result { BufferId::new(message.buffer_id) } } pub struct ParsedCompletionEdit { pub replace_range: Range, pub insert_range: Option>, pub new_text: String, } pub(crate) fn parse_completion_text_edit( edit: &lsp::CompletionTextEdit, snapshot: &BufferSnapshot, ) -> Option { 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"); 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: insert_range, replace_range: replace_range, new_text: new_text.clone(), }) } #[async_trait(?Send)] impl LspCommand for GetCodeActions { type Response = Vec; 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::>(); requested.iter().any(|kind| server_supported.contains(kind)) } else { true } } } } fn to_lsp( &self, path: &Path, buffer: &Buffer, language_server: &Arc, _: &App, ) -> Result { 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::>(); 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_store: Entity, _: Entity, server_id: LanguageServerId, cx: AsyncApp, ) -> Result> { let requested_kinds_set = if let Some(kinds) = self.kinds { Some(kinds.into_iter().collect::>()) } else { None }; 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() { if !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()) { if !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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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, _: &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, buffer: Entity, mut cx: AsyncApp, ) -> Result> { 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::new(message.buffer_id) } } impl GetCodeActions { fn supported_code_action_kinds( capabilities: AdapterServerCapabilities, ) -> Option> { match capabilities.server_capabilities.code_action_provider { Some(lsp::CodeActionProviderCapability::Options(CodeActionOptions { code_action_kinds: Some(supported_action_kinds), .. })) => Some(supported_action_kinds.clone()), _ => 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) } } #[async_trait(?Send)] impl LspCommand for OnTypeFormatting { type Response = Option; type LspRequest = lsp::request::OnTypeFormatting; type ProtoRequest = proto::OnTypeFormatting; fn display_name(&self) -> &str { "Formatting on typing" } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { let Some(on_type_formatting_options) = &capabilities .server_capabilities .document_on_type_formatting_provider else { return false; }; on_type_formatting_options .first_trigger_character .contains(&self.trigger) || on_type_formatting_options .more_trigger_character .iter() .flatten() .any(|chars| chars.contains(&self.trigger)) } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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>, lsp_store: Entity, buffer: Entity, server_id: LanguageServerId, mut cx: AsyncApp, ) -> Result> { 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, trigger: message.trigger.clone(), options, push_to_history: false, }) } fn response_to_proto( response: Option, _: &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, _: Entity, _: AsyncApp, ) -> Result> { 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::new(message.buffer_id) } } impl InlayHints { pub async fn lsp_to_project_hint( lsp_hint: lsp::InlayHint, buffer_handle: &Entity, server_id: LanguageServerId, resolve_state: ResolveState, force_no_type_left_padding: bool, cx: &mut AsyncApp, ) -> anyhow::Result { 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 { 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 { 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::>(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) } } #[async_trait(?Send)] impl LspCommand for InlayHints { type Response = Vec; type LspRequest = lsp::InlayHintRequest; type ProtoRequest = proto::InlayHints; fn display_name(&self) -> &str { "Inlay hints" } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { let Some(inlay_hint_provider) = &capabilities.server_capabilities.inlay_hint_provider else { return false; }; match inlay_hint_provider { lsp::OneOf::Left(enabled) => *enabled, lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { lsp::InlayHintServerCapabilities::Options(_) => true, lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false, }, } } fn to_lsp( &self, path: &Path, buffer: &Buffer, _: &Arc, _: &App, ) -> Result { 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>, lsp_store: Entity, buffer: Entity, server_id: LanguageServerId, mut cx: AsyncApp, ) -> anyhow::Result> { 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::>() .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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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, _: &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, buffer: Entity, mut cx: AsyncApp, ) -> anyhow::Result> { 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::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GetCodeLens { type Response = Vec; 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 .as_ref() .map_or(false, |code_lens_options| { code_lens_options.resolve_provider.unwrap_or(false) }) } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { 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>, lsp_store: Entity, buffer: Entity, server_id: LanguageServerId, mut cx: AsyncApp, ) -> anyhow::Result> { let snapshot = buffer.read_with(&mut 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) })? .await?; Ok(Self) } fn response_to_proto( response: Vec, _: &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, buffer: Entity, mut cx: AsyncApp, ) -> anyhow::Result> { 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::>>() .context("deserializing proto code lens response") } fn buffer_id_from_proto(message: &proto::GetCodeLens) -> Result { BufferId::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for LinkedEditingRange { type Response = Vec>; type LspRequest = lsp::request::LinkedEditingRange; type ProtoRequest = proto::LinkedEditingRange; fn display_name(&self) -> &str { "Linked editing range" } fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { let Some(linked_editing_options) = &capabilities .server_capabilities .linked_editing_range_provider else { return false; }; if let LinkedEditingRangeServerCapabilities::Simple(false) = linked_editing_options { return false; } true } fn to_lsp( &self, path: &Path, buffer: &Buffer, _server: &Arc, _: &App, ) -> Result { 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, _: Entity, buffer: Entity, _server_id: LanguageServerId, cx: AsyncApp, ) -> Result>> { 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, buffer: Entity, mut cx: AsyncApp, ) -> Result { 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>, _: &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, buffer: Entity, mut cx: AsyncApp, ) -> Result>> { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) })? .await?; let items: Vec> = 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::new(message.buffer_id) } } impl GetDocumentDiagnostics { fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result { let start = diagnostic.start.context("invalid start range")?; let end = diagnostic.end.context("invalid end range")?; let range = Range:: { 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.clone(), } }) .collect::>(); 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::>(); 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: match diagnostic.code_description { Some(code_description) => Some(CodeDescription { href: lsp::Url::parse(&code_description).unwrap(), }), None => None, }, related_information: Some(related_information), tags: Some(tags), source: diagnostic.source.clone(), message: diagnostic.message, data, }) } fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result { 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::>>()?; 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 .map(|desc| desc.href.to_string()), message: diagnostic.message, data: diagnostic.data.as_ref().map(|data| data.to_string()), }) } } #[async_trait(?Send)] impl LspCommand for GetDocumentDiagnostics { type Response = Vec; 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, _: &App, ) -> Result { 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: None, partial_result_params: Default::default(), work_done_progress_params: Default::default(), }) } async fn response_from_lsp( self, message: lsp::DocumentDiagnosticReportResult, _: Entity, buffer: Entity, server_id: LanguageServerId, cx: AsyncApp, ) -> Result { 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( message: proto::GetDocumentDiagnostics, _: Entity, buffer: Entity, mut cx: AsyncApp, ) -> Result { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) })? .await?; Ok(Self {}) } 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, _: Entity, _: AsyncApp, ) -> Result { let pulled_diagnostics = 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(); Ok(pulled_diagnostics) } fn buffer_id_from_proto(message: &proto::GetDocumentDiagnostics) -> Result { BufferId::new(message.buffer_id) } } fn process_related_documents( diagnostics: &mut HashMap, server_id: LanguageServerId, documents: impl IntoIterator, ) { 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, 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, 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.clone()) .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()); } }