#![allow(rustdoc::invalid_codeblock_attributes)]
use std::collections::HashMap;
use std::fmt::{self, Display};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use async_trait::async_trait;
use nvim_rs::{Handler, Neovim, create::tokio as create};
use rmpv::Value;
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer};
use tokio::{
io::AsyncWrite,
net::TcpStream,
sync::Mutex,
time::{Duration, timeout},
};
use tracing::{debug, info, instrument};
use super::{connection::NeovimConnection, error::NeovimError};
#[async_trait]
pub trait NeovimClientTrait: Sync {
fn target(&self) -> Option<String>;
async fn disconnect(&mut self) -> Result<String, NeovimError>;
async fn get_buffers(&self) -> Result<Vec<BufferInfo>, NeovimError>;
async fn execute_lua(&self, code: &str) -> Result<Value, NeovimError>;
async fn setup_autocmd(&self) -> Result<(), NeovimError>;
async fn wait_for_notification(
&self,
notification_name: &str,
timeout_ms: u64,
) -> Result<Notification, NeovimError>;
async fn wait_for_lsp_ready(
&self,
client_name: Option<&str>,
timeout_ms: u64,
) -> Result<(), NeovimError>;
async fn wait_for_diagnostics(
&self,
buffer_id: Option<u64>,
timeout_ms: u64,
) -> Result<Vec<Diagnostic>, NeovimError>;
async fn get_buffer_diagnostics(&self, buffer_id: u64) -> Result<Vec<Diagnostic>, NeovimError>;
async fn get_workspace_diagnostics(&self) -> Result<Vec<Diagnostic>, NeovimError>;
async fn lsp_get_clients(&self) -> Result<Vec<LspClient>, NeovimError>;
async fn lsp_get_code_actions(
&self,
client_name: &str,
document: DocumentIdentifier,
range: Range,
) -> Result<Vec<CodeAction>, NeovimError>;
async fn lsp_hover(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<HoverResult, NeovimError>;
async fn lsp_document_symbols(
&self,
client_name: &str,
document: DocumentIdentifier,
) -> Result<Option<DocumentSymbolResult>, NeovimError>;
async fn lsp_workspace_symbols(
&self,
client_name: &str,
query: &str,
) -> Result<Option<DocumentSymbolResult>, NeovimError>;
async fn lsp_references(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
include_declaration: bool,
) -> Result<Vec<Location>, NeovimError>;
async fn lsp_definition(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<LocateResult>, NeovimError>;
async fn lsp_type_definition(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<LocateResult>, NeovimError>;
async fn lsp_implementation(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<LocateResult>, NeovimError>;
async fn lsp_declaration(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<LocateResult>, NeovimError>;
async fn lsp_resolve_code_action(
&self,
client_name: &str,
code_action: CodeAction,
) -> Result<CodeAction, NeovimError>;
async fn lsp_apply_workspace_edit(
&self,
client_name: &str,
workspace_edit: WorkspaceEdit,
) -> Result<(), NeovimError>;
async fn lsp_prepare_rename(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<PrepareRenameResult>, NeovimError>;
async fn lsp_rename(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
new_name: &str,
) -> Result<Option<WorkspaceEdit>, NeovimError>;
async fn lsp_formatting(
&self,
client_name: &str,
document: DocumentIdentifier,
options: FormattingOptions,
) -> Result<Vec<TextEdit>, NeovimError>;
async fn lsp_range_formatting(
&self,
client_name: &str,
document: DocumentIdentifier,
range: Range,
options: FormattingOptions,
) -> Result<Vec<TextEdit>, NeovimError>;
async fn lsp_get_organize_imports_actions(
&self,
client_name: &str,
document: DocumentIdentifier,
) -> Result<Vec<CodeAction>, NeovimError>;
async fn lsp_apply_text_edits(
&self,
client_name: &str,
document: DocumentIdentifier,
text_edits: Vec<TextEdit>,
) -> Result<(), NeovimError>;
async fn navigate(
&self,
document: DocumentIdentifier,
position: Position,
) -> Result<NavigateResult, NeovimError>;
async fn lsp_call_hierarchy_prepare(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<Vec<CallHierarchyItem>>, NeovimError>;
async fn lsp_call_hierarchy_incoming_calls(
&self,
client_name: &str,
item: CallHierarchyItem,
) -> Result<Option<Vec<CallHierarchyIncomingCall>>, NeovimError>;
async fn lsp_call_hierarchy_outgoing_calls(
&self,
client_name: &str,
item: CallHierarchyItem,
) -> Result<Option<Vec<CallHierarchyOutgoingCall>>, NeovimError>;
async fn lsp_type_hierarchy_prepare(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<Vec<TypeHierarchyItem>>, NeovimError>;
async fn lsp_type_hierarchy_supertypes(
&self,
client_name: &str,
item: TypeHierarchyItem,
) -> Result<Option<Vec<TypeHierarchyItem>>, NeovimError>;
async fn lsp_type_hierarchy_subtypes(
&self,
client_name: &str,
item: TypeHierarchyItem,
) -> Result<Option<Vec<TypeHierarchyItem>>, NeovimError>;
async fn read_document(
&self,
document: DocumentIdentifier,
start: i64,
end: i64,
) -> Result<String, NeovimError>;
}
#[derive(Debug, Clone)]
pub struct Notification {
pub name: String,
pub args: Vec<Value>,
pub timestamp: std::time::SystemTime,
}
#[derive(Clone, Default)]
pub struct NotificationTracker {
notifications: Arc<Mutex<Vec<Notification>>>,
notify_wakers: Arc<Mutex<HashMap<String, Vec<tokio::sync::oneshot::Sender<Notification>>>>>,
}
const MAX_STORED_NOTIFICATIONS: usize = 100;
const NOTIFICATION_EXPIRY_SECONDS: u64 = 30;
impl NotificationTracker {
async fn cleanup_notifications(&self) {
let mut notifications = self.notifications.lock().await;
let now = std::time::SystemTime::now();
notifications.retain(|n| {
now.duration_since(n.timestamp)
.map(|d| d.as_secs() < NOTIFICATION_EXPIRY_SECONDS)
.unwrap_or(false)
});
if notifications.len() > MAX_STORED_NOTIFICATIONS {
let excess = notifications.len() - MAX_STORED_NOTIFICATIONS;
notifications.drain(0..excess);
}
}
pub async fn record_notification(&self, name: String, args: Vec<Value>) {
let notification = Notification {
name: name.clone(),
args,
timestamp: std::time::SystemTime::now(),
};
let mut wakers = self.notify_wakers.lock().await;
if let Some(waiters) = wakers.get_mut(&name) {
while let Some(waker) = waiters.pop() {
let _ = waker.send(notification.clone());
}
}
wakers.retain(|_, waiters| !waiters.is_empty());
drop(wakers);
{
let mut notifications = self.notifications.lock().await;
notifications.push(notification);
if notifications.len() > MAX_STORED_NOTIFICATIONS * 3 / 4 {
drop(notifications); self.cleanup_notifications().await;
}
}
}
pub async fn wait_for_notification(
&self,
notification_name: &str,
timeout_duration: Duration,
) -> Result<Notification, NeovimError> {
{
let notifications = self.notifications.lock().await;
let now = std::time::SystemTime::now();
if let Some(notification) = notifications
.iter()
.rev() .find(|n| {
n.name == notification_name
&& now
.duration_since(n.timestamp)
.map(|d| d.as_secs() < NOTIFICATION_EXPIRY_SECONDS)
.unwrap_or(false)
})
{
return Ok(notification.clone());
}
}
let (tx, rx) = tokio::sync::oneshot::channel();
let mut wakers = self.notify_wakers.lock().await;
wakers
.entry(notification_name.to_string())
.or_insert_with(Vec::new)
.push(tx);
drop(wakers);
match timeout(timeout_duration, rx).await {
Ok(Ok(notification)) => Ok(notification),
Ok(Err(_)) => Err(NeovimError::Api(
"Notification channel closed unexpectedly".to_string(),
)),
Err(_) => Err(NeovimError::Api(format!(
"Timeout waiting for notification: {}",
notification_name
))),
}
}
pub async fn clear_notifications(&self) {
let mut notifications = self.notifications.lock().await;
notifications.clear();
}
#[allow(dead_code)]
pub(crate) async fn cleanup_expired_notifications(&self) {
self.cleanup_notifications().await;
}
#[allow(dead_code)]
pub(crate) async fn get_stats(&self) -> (usize, usize) {
let notifications = self.notifications.lock().await;
let wakers = self.notify_wakers.lock().await;
(notifications.len(), wakers.len())
}
}
pub struct NeovimHandler<T> {
_marker: std::marker::PhantomData<T>,
notification_tracker: NotificationTracker,
}
impl<T> NeovimHandler<T> {
pub fn new() -> Self {
NeovimHandler {
_marker: std::marker::PhantomData,
notification_tracker: NotificationTracker::default(),
}
}
pub fn notification_tracker(&self) -> NotificationTracker {
self.notification_tracker.clone()
}
}
impl<T> Clone for NeovimHandler<T> {
fn clone(&self) -> Self {
NeovimHandler {
_marker: std::marker::PhantomData,
notification_tracker: self.notification_tracker.clone(),
}
}
}
#[async_trait]
impl<T> Handler for NeovimHandler<T>
where
T: futures::AsyncWrite + Send + Sync + Unpin + 'static,
{
type Writer = T;
async fn handle_notify(&self, name: String, args: Vec<Value>, _neovim: Neovim<T>) {
info!("handling notification: {name:?}, {args:?}");
self.notification_tracker
.record_notification(name, args)
.await;
}
async fn handle_request(
&self,
name: String,
args: Vec<Value>,
_neovim: Neovim<T>,
) -> Result<Value, Value> {
info!("handling request: {name:?}, {args:?}");
match name.as_ref() {
"ping" => Ok(Value::from("pong")),
_ => Ok(Value::Nil),
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Diagnostic {
pub message: String,
pub code: Option<serde_json::Value>,
pub severity: u8,
pub lnum: u64,
pub col: u64,
pub source: String,
pub bufnr: u64,
pub end_lnum: u64,
pub end_col: u64,
pub namespace: u64,
pub user_data: Option<UserData>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct UserData {
pub lsp: LSPDiagnostic,
#[serde(flatten)]
pub unknowns: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
pub struct LSPDiagnostic {
pub code: Option<serde_json::Value>,
pub message: String,
pub range: Range,
pub severity: u8,
pub source: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct LspClient {
pub id: u64,
pub name: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BufferInfo {
pub id: u64,
pub name: String,
pub line_count: u64,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
pub struct TextDocumentIdentifier {
uri: String,
version: Option<i32>,
}
struct StringOrStruct<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for StringOrStruct<T>
where
T: Deserialize<'de> + FromStr,
<T as FromStr>::Err: Display,
{
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<T, E>
where
E: de::Error,
{
FromStr::from_str(value).map_err(de::Error::custom)
}
fn visit_map<M>(self, map: M) -> Result<T, M::Error>
where
M: MapAccess<'de>,
{
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
}
}
pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de> + FromStr,
<T as FromStr>::Err: Display,
{
deserializer.deserialize_any(StringOrStruct(PhantomData))
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DocumentIdentifier {
BufferId(u64),
ProjectRelativePath(PathBuf),
AbsolutePath(PathBuf),
}
macro_rules! impl_fromstr_serde_json {
($type:ty) => {
impl FromStr for $type {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
};
}
impl_fromstr_serde_json!(DocumentIdentifier);
impl DocumentIdentifier {
pub fn from_buffer_id(buffer_id: u64) -> Self {
Self::BufferId(buffer_id)
}
pub fn from_project_path<P: Into<PathBuf>>(path: P) -> Self {
Self::ProjectRelativePath(path.into())
}
pub fn from_absolute_path<P: Into<PathBuf>>(path: P) -> Self {
Self::AbsolutePath(path.into())
}
}
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
pub struct Position {
pub line: u64,
pub character: u64,
}
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
pub struct Range {
pub start: Position,
pub end: Position,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum CodeActionKind {
#[serde(rename = "")]
Empty,
#[serde(rename = "quickfix")]
Quickfix,
Refactor,
#[serde(rename = "refactor.extract")]
RefactorExtract,
#[serde(rename = "refactor.inline")]
RefactorInline,
#[serde(rename = "refactor.rewrite")]
RefactorRewrite,
Source,
#[serde(rename = "source.organizeImports")]
SourceOrganizeImports,
#[serde(rename = "source.fixAll")]
SourceFixAll,
#[serde(untagged)]
Unknown(String),
}
#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
pub enum CodeActionTriggerKind {
Invoked = 1,
Automatic = 2,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionContext {
only: Option<Vec<CodeActionKind>>,
diagnostics: Vec<LSPDiagnostic>,
trigger_kind: Option<CodeActionTriggerKind>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionParams {
pub text_document: TextDocumentIdentifier,
pub range: Range,
pub context: CodeActionContext,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
pub struct Disabled {
reason: String,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TextEdit {
range: Range,
new_text: String,
annotation_id: Option<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceEdit {
#[serde(skip_serializing_if = "Option::is_none")]
changes: Option<std::collections::HashMap<String, Vec<TextEdit>>>,
#[serde(skip_serializing_if = "Option::is_none")]
document_changes: Option<Vec<serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
change_annotations: Option<HashMap<String, serde_json::Value>>,
}
impl_fromstr_serde_json!(WorkspaceEdit);
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct FormattingOptions {
pub tab_size: u32,
pub insert_spaces: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub trim_trailing_whitespace: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_final_newline: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trim_final_newlines: Option<bool>,
#[serde(flatten)]
pub extras: HashMap<String, serde_json::Value>,
}
impl_fromstr_serde_json!(FormattingOptions);
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
pub struct Command {
title: String,
command: String,
arguments: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CodeAction {
title: String,
kind: Option<CodeActionKind>,
diagnostics: Option<Vec<LSPDiagnostic>>,
is_preferred: Option<bool>,
disabled: Option<Disabled>,
edit: Option<WorkspaceEdit>,
command: Option<Command>,
data: Option<serde_json::Value>,
}
impl CodeAction {
pub fn title(&self) -> &str {
&self.title
}
pub fn edit(&self) -> Option<&WorkspaceEdit> {
self.edit.as_ref()
}
pub fn has_edit(&self) -> bool {
self.edit.is_some()
}
}
impl_fromstr_serde_json!(CodeAction);
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TextDocumentPositionParams {
pub text_document: TextDocumentIdentifier,
pub position: Position,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferenceParams {
pub text_document: TextDocumentIdentifier,
pub position: Position,
pub context: ReferenceContext,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReferenceContext {
pub include_declaration: bool,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct HoverResult {
pub contents: HoverContents,
pub range: Option<Range>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum HoverContents {
String(MarkedString),
Strings(Vec<MarkedString>),
Content(MarkupContent),
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub enum MarkedString {
String(String),
Markup { lang: String, value: String },
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct MarkupContent {
pub kind: MarkupKind,
pub value: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub enum MarkupKind {
#[serde(rename = "plaintext")]
PlainText,
#[serde(rename = "markdown")]
Markdown,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct CodeActionResult {
#[serde(default)]
pub result: Vec<CodeAction>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct NavigateResult {
pub success: bool,
pub buffer_name: String,
pub line: String,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(into = "u8", from = "u8")]
pub enum SymbolKind {
File = 1,
Module = 2,
Namespace = 3,
Package = 4,
Class = 5,
Method = 6,
Property = 7,
Field = 8,
Constructor = 9,
Enum = 10,
Interface = 11,
Function = 12,
Variable = 13,
Constant = 14,
String = 15,
Number = 16,
Boolean = 17,
Array = 18,
Object = 19,
Key = 20,
Null = 21,
EnumMember = 22,
Struct = 23,
Event = 24,
Operator = 25,
TypeParameter = 26,
}
impl From<SymbolKind> for u8 {
fn from(kind: SymbolKind) -> u8 {
kind as u8
}
}
impl From<u8> for SymbolKind {
fn from(value: u8) -> SymbolKind {
match value {
1 => SymbolKind::File,
2 => SymbolKind::Module,
3 => SymbolKind::Namespace,
4 => SymbolKind::Package,
5 => SymbolKind::Class,
6 => SymbolKind::Method,
7 => SymbolKind::Property,
8 => SymbolKind::Field,
9 => SymbolKind::Constructor,
10 => SymbolKind::Enum,
11 => SymbolKind::Interface,
12 => SymbolKind::Function,
13 => SymbolKind::Variable,
14 => SymbolKind::Constant,
15 => SymbolKind::String,
16 => SymbolKind::Number,
17 => SymbolKind::Boolean,
18 => SymbolKind::Array,
19 => SymbolKind::Object,
20 => SymbolKind::Key,
21 => SymbolKind::Null,
22 => SymbolKind::EnumMember,
23 => SymbolKind::Struct,
24 => SymbolKind::Event,
25 => SymbolKind::Operator,
26 => SymbolKind::TypeParameter,
_ => SymbolKind::Variable, }
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(into = "u8", from = "u8")]
pub enum SymbolTag {
Deprecated = 1,
}
impl From<SymbolTag> for u8 {
fn from(tag: SymbolTag) -> u8 {
tag as u8
}
}
impl From<u8> for SymbolTag {
fn from(value: u8) -> SymbolTag {
match value {
1 => SymbolTag::Deprecated,
_ => SymbolTag::Deprecated, }
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Location {
pub uri: String,
pub range: Range,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LocationLink {
pub origin_selection_range: Option<Range>,
pub target_uri: String,
pub target_range: Range,
pub target_selection_range: Range,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum LocateResult {
Single(Location),
Locations(Vec<Location>),
LocationLinks(Vec<LocationLink>),
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SymbolInformation {
pub name: String,
pub kind: SymbolKind,
pub tags: Option<Vec<SymbolTag>>,
pub deprecated: Option<bool>,
pub location: Location,
pub container_name: Option<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentSymbol {
pub name: String,
pub detail: Option<String>,
pub kind: SymbolKind,
pub tags: Option<Vec<SymbolTag>>,
pub deprecated: Option<bool>,
pub range: Range,
pub selection_range: Range,
pub children: Option<Vec<DocumentSymbol>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentSymbolParams {
pub text_document: TextDocumentIdentifier,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct WorkspaceSymbolParams {
pub query: String,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyItem {
pub name: String,
pub kind: SymbolKind,
pub tags: Option<Vec<SymbolTag>>,
pub detail: Option<String>,
pub uri: String,
pub range: Range,
pub selection_range: Range,
pub data: Option<serde_json::Value>,
}
impl_fromstr_serde_json!(CallHierarchyItem);
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyPrepareParams {
pub text_document: TextDocumentIdentifier,
pub position: Position,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyIncomingCall {
pub from: CallHierarchyItem,
pub from_ranges: Vec<Range>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyIncomingCallsParams {
pub item: CallHierarchyItem,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyOutgoingCall {
pub to: CallHierarchyItem,
pub from_ranges: Vec<Range>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyOutgoingCallsParams {
pub item: CallHierarchyItem,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TypeHierarchyItem {
pub name: String,
pub kind: SymbolKind,
pub tags: Option<Vec<SymbolTag>>,
pub detail: Option<String>,
pub uri: String,
pub range: Range,
pub selection_range: Range,
pub data: Option<serde_json::Value>,
}
impl_fromstr_serde_json!(TypeHierarchyItem);
#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TypeHierarchyPrepareParams {
pub text_document: TextDocumentIdentifier,
pub position: Position,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TypeHierarchySupertypesParams {
pub item: TypeHierarchyItem,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TypeHierarchySubtypesParams {
pub item: TypeHierarchyItem,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum DocumentSymbolResult {
Symbols(Vec<DocumentSymbol>),
Information(Vec<SymbolInformation>),
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum PrepareRenameResult {
Range(Range),
RangeWithPlaceholder {
range: Range,
placeholder: String,
},
DefaultBehavior {
#[serde(rename = "defaultBehavior")]
default_behavior: bool,
},
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RenameRequestParams {
pub text_document: TextDocumentIdentifier,
pub position: Position,
pub new_name: String,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ReadDocumentParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub buffer_id: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
pub start_line: i64,
pub end_line: i64,
}
impl ReadDocumentParams {
fn buffer_id(id: u64, start: i64, end: i64) -> Self {
Self {
buffer_id: Some(id),
file_path: None,
start_line: start,
end_line: end,
}
}
fn path<P: AsRef<Path>>(path: P, start: i64, end: i64) -> Self {
Self {
buffer_id: None,
file_path: Some(path.as_ref().to_string_lossy().to_string()),
start_line: start,
end_line: end,
}
}
}
#[derive(Debug, Clone)]
pub struct NeovimClientConfig {
pub lsp_timeout_ms: u64,
}
impl Default for NeovimClientConfig {
fn default() -> Self {
Self {
lsp_timeout_ms: 3000,
}
}
}
pub struct NeovimClient<T>
where
T: AsyncWrite + Send + 'static,
{
connection: Option<NeovimConnection<T>>,
notification_tracker: Option<NotificationTracker>,
config: NeovimClientConfig,
}
impl<T> Default for NeovimClient<T>
where
T: AsyncWrite + Send + 'static,
{
fn default() -> Self {
Self {
connection: None,
notification_tracker: None,
config: NeovimClientConfig::default(),
}
}
}
#[cfg(unix)]
type Connection = tokio::net::UnixStream;
#[cfg(windows)]
type Connection = tokio::net::windows::named_pipe::NamedPipeClient;
#[allow(dead_code)]
pub fn make_text_document_identifier_from_path<P: AsRef<Path>>(
file_path: P,
) -> Result<TextDocumentIdentifier, NeovimError> {
let path = file_path.as_ref();
let absolute_path = path.canonicalize().map_err(|e| {
NeovimError::Api(format!("Failed to resolve path {}: {}", path.display(), e))
})?;
let uri = format!("file://{}", absolute_path.display());
Ok(TextDocumentIdentifier {
uri,
version: None, })
}
#[derive(Debug, serde::Deserialize)]
pub enum NvimExecuteLuaResult<T> {
#[serde(rename = "err_msg")]
Error(String),
#[serde(rename = "result")]
Ok(T),
#[serde(rename = "err")]
LspError { message: String, code: i32 },
}
impl<T> From<NvimExecuteLuaResult<T>> for Result<T, NeovimError> {
fn from(val: NvimExecuteLuaResult<T>) -> Self {
use NvimExecuteLuaResult::*;
match val {
Ok(result) => Result::Ok(result),
Error(msg) => Err(NeovimError::Api(msg)),
LspError { message, code } => Err(NeovimError::Lsp { code, message }),
}
}
}
impl NeovimClient<Connection> {
#[instrument(skip(self))]
pub async fn connect_path(&mut self, path: &str) -> Result<(), NeovimError> {
if self.connection.is_some() {
return Err(NeovimError::Connection(format!(
"Already connected to {}. Disconnect first.",
self.connection.as_ref().unwrap().target()
)));
}
debug!("Attempting to connect to Neovim at {}", path);
let handler = NeovimHandler::new();
let notification_tracker = handler.notification_tracker();
match create::new_path(path, handler).await {
Ok((nvim, io_handler)) => {
let connection = NeovimConnection::new(
nvim,
tokio::spawn(async move {
let rv = io_handler.await;
info!("io_handler completed with result: {:?}", rv);
rv
}),
path.to_string(),
);
self.connection = Some(connection);
self.notification_tracker = Some(notification_tracker);
debug!("Successfully connected to Neovim at {}", path);
Ok(())
}
Err(e) => {
debug!("Failed to connect to Neovim at {}: {}", path, e);
Err(NeovimError::Connection(format!("Connection failed: {e}")))
}
}
}
}
impl NeovimClient<TcpStream> {
#[instrument(skip(self))]
pub async fn connect_tcp(&mut self, address: &str) -> Result<(), NeovimError> {
if self.connection.is_some() {
return Err(NeovimError::Connection(format!(
"Already connected to {}. Disconnect first.",
self.connection.as_ref().unwrap().target()
)));
}
debug!("Attempting to connect to Neovim at {}", address);
let handler = NeovimHandler::new();
let notification_tracker = handler.notification_tracker();
match create::new_tcp(address, handler).await {
Ok((nvim, io_handler)) => {
let connection = NeovimConnection::new(
nvim,
tokio::spawn(async move {
let rv = io_handler.await;
info!("io_handler completed with result: {:?}", rv);
rv
}),
address.to_string(),
);
self.connection = Some(connection);
self.notification_tracker = Some(notification_tracker);
debug!("Successfully connected to Neovim at {}", address);
Ok(())
}
Err(e) => {
debug!("Failed to connect to Neovim at {}: {}", address, e);
Err(NeovimError::Connection(format!("Connection failed: {e}")))
}
}
}
}
impl<T> NeovimClient<T>
where
T: AsyncWrite + Send + 'static,
{
#[allow(dead_code)]
pub fn with_config(mut self, config: NeovimClientConfig) -> Self {
self.config = config;
self
}
#[instrument(skip(self))]
async fn get_diagnostics(
&self,
buffer_id: Option<u64>,
) -> Result<Vec<Diagnostic>, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let args = if let Some(id) = buffer_id {
vec![Value::from(id)]
} else {
vec![]
};
match conn
.nvim
.execute_lua("return vim.json.encode(vim.diagnostic.get(...))", args)
.await
{
Ok(diagnostics) => {
let diagnostics: Vec<Diagnostic> =
match serde_json::from_str(diagnostics.as_str().unwrap()) {
Ok(d) => d,
Err(e) => {
debug!("Failed to parse diagnostics: {}", e);
return Err(NeovimError::Api(format!(
"Failed to parse diagnostics: {e}"
)));
}
};
debug!("Found {} diagnostics", diagnostics.len());
Ok(diagnostics)
}
Err(e) => {
debug!("Failed to get diagnostics: {}", e);
Err(NeovimError::Api(format!("Failed to get diagnostics: {e}")))
}
}
}
#[instrument(skip(self))]
async fn lsp_make_text_document_params(
&self,
buffer_id: u64,
) -> Result<TextDocumentIdentifier, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_make_text_document_params.lua"),
vec![Value::from(buffer_id)],
)
.await
{
Ok(raw) => {
let doc = serde_json::from_str::<TextDocumentIdentifier>(raw.as_str().unwrap())
.map_err(|e| {
NeovimError::Api(format!("Failed to parse text document params: {e}"))
})?;
info!("Created text document params {doc:?} for buffer {buffer_id}");
Ok(doc)
}
Err(e) => {
debug!("Failed to make text document params: {}", e);
Err(NeovimError::Api(format!(
"Failed to make text document params: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn get_project_root(&self) -> Result<PathBuf, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua("return vim.fn.getcwd()", vec![])
.await
{
Ok(value) => {
let cwd = value.as_str().ok_or_else(|| {
NeovimError::Api("Invalid working directory format".to_string())
})?;
Ok(PathBuf::from(cwd))
}
Err(e) => Err(NeovimError::Api(format!(
"Failed to get working directory: {e}"
))),
}
}
#[instrument(skip(self))]
async fn resolve_text_document_identifier(
&self,
identifier: &DocumentIdentifier,
) -> Result<TextDocumentIdentifier, NeovimError> {
match identifier {
DocumentIdentifier::BufferId(buffer_id) => {
self.lsp_make_text_document_params(*buffer_id).await
}
DocumentIdentifier::ProjectRelativePath(rel_path) => {
let project_root = self.get_project_root().await?;
let absolute_path = project_root.join(rel_path);
make_text_document_identifier_from_path(absolute_path)
}
DocumentIdentifier::AbsolutePath(abs_path) => {
make_text_document_identifier_from_path(abs_path)
}
}
}
}
#[async_trait]
impl<T> NeovimClientTrait for NeovimClient<T>
where
T: AsyncWrite + Send + 'static,
{
fn target(&self) -> Option<String> {
self.connection.as_ref().map(|c| c.target().to_string())
}
#[instrument(skip(self))]
async fn disconnect(&mut self) -> Result<String, NeovimError> {
debug!("Attempting to disconnect from Neovim");
if let Some(connection) = self.connection.take() {
let target = connection.target().to_string();
connection.io_handler.abort();
if let Some(tracker) = self.notification_tracker.take() {
tracker.clear_notifications().await;
}
debug!("Successfully disconnected from Neovim at {}", target);
Ok(target)
} else {
Err(NeovimError::Connection(
"Not connected to any Neovim instance".to_string(),
))
}
}
#[instrument(skip(self))]
async fn get_buffers(&self) -> Result<Vec<BufferInfo>, NeovimError> {
debug!("Getting buffer information");
let lua_code = include_str!("lua/lsp_get_buffers.lua");
match self.execute_lua(lua_code).await {
Ok(buffers) => {
debug!("Get buffers retrieved successfully");
let buffers: Vec<BufferInfo> = match serde_json::from_str(buffers.as_str().unwrap())
{
Ok(d) => d,
Err(e) => {
debug!("Failed to parse buffers: {}", e);
return Err(NeovimError::Api(format!("Failed to parse buffers: {e}")));
}
};
debug!("Found {} buffers", buffers.len());
Ok(buffers)
}
Err(e) => {
debug!("Failed to get buffer info: {}", e);
Err(NeovimError::Api(format!("Failed to get buffer info: {e}")))
}
}
}
#[instrument(skip(self))]
async fn execute_lua(&self, code: &str) -> Result<Value, NeovimError> {
debug!("Executing Lua code: {}", code);
if code.trim().is_empty() {
return Err(NeovimError::Api("Lua code cannot be empty".to_string()));
}
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let lua_args = Vec::<Value>::new();
match conn.nvim.exec_lua(code, lua_args).await {
Ok(result) => {
debug!("Lua execution successful, result: {:?}", result);
Ok(result)
}
Err(e) => {
debug!("Lua execution failed: {e}");
Err(NeovimError::Api(format!("Lua execution failed: {e}")))
}
}
}
#[instrument(skip(self))]
async fn setup_autocmd(&self) -> Result<(), NeovimError> {
debug!("Setting up autocmd");
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.exec_lua(include_str!("lua/setup_autocmd.lua"), vec![])
.await
{
Ok(_) => {
debug!("autocmd set up successfully");
Ok(())
}
Err(e) => {
debug!("Failed to set up autocmd: {}", e);
Err(NeovimError::Api(format!("Failed to set up autocmd: {e}")))
}
}
}
#[instrument(skip(self))]
async fn get_buffer_diagnostics(&self, buffer_id: u64) -> Result<Vec<Diagnostic>, NeovimError> {
self.get_diagnostics(Some(buffer_id)).await
}
#[instrument(skip(self))]
async fn get_workspace_diagnostics(&self) -> Result<Vec<Diagnostic>, NeovimError> {
self.get_diagnostics(None).await
}
#[instrument(skip(self))]
async fn lsp_get_clients(&self) -> Result<Vec<LspClient>, NeovimError> {
debug!("Getting LSP clients");
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(include_str!("lua/lsp_get_clients.lua"), vec![])
.await
{
Ok(clients) => {
debug!("LSP clients retrieved successfully");
let clients: Vec<LspClient> = match serde_json::from_str(clients.as_str().unwrap())
{
Ok(d) => d,
Err(e) => {
debug!("Failed to parse clients: {}", e);
return Err(NeovimError::Api(format!("Failed to parse clients: {e}")));
}
};
debug!("Found {} clients", clients.len());
Ok(clients)
}
Err(e) => {
debug!("Failed to get LSP clients: {}", e);
Err(NeovimError::Api(format!("Failed to get LSP clients: {e}")))
}
}
}
#[instrument(skip(self))]
async fn lsp_get_code_actions(
&self,
client_name: &str,
document: DocumentIdentifier,
range: Range,
) -> Result<Vec<CodeAction>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let diagnostics = match &document {
DocumentIdentifier::BufferId(buffer_id) => self
.get_buffer_diagnostics(*buffer_id)
.await
.map_err(|e| NeovimError::Api(format!("Failed to get diagnostics: {e}")))?,
_ => {
Vec::new()
}
};
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let buffer_id = match &document {
DocumentIdentifier::BufferId(id) => *id,
_ => 0, };
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_client_get_code_actions.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&CodeActionParams {
text_document,
range,
context: CodeActionContext {
diagnostics: diagnostics
.into_iter()
.filter_map(|d| d.user_data.map(|u| u.lsp))
.collect(),
only: None,
trigger_kind: None,
},
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), Value::from(buffer_id), ],
)
.await
{
Ok(actions) => {
let actions = serde_json::from_str::<CodeActionResult>(actions.as_str().unwrap())
.map_err(|e| {
NeovimError::Api(format!("Failed to parse code actions: {e}"))
})?;
debug!("Found {} code actions", actions.result.len());
Ok(actions.result)
}
Err(e) => {
debug!("Failed to get LSP code actions: {}", e);
Err(NeovimError::Api(format!(
"Failed to get LSP code actions: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_hover(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<HoverResult, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let buffer_id = match &document {
DocumentIdentifier::BufferId(id) => *id,
_ => 0, };
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_hover.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&TextDocumentPositionParams {
text_document,
position,
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), Value::from(buffer_id), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<HoverResult>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse hover result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse hover result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get LSP hover: {}", e);
Err(NeovimError::Api(format!("Failed to get LSP hover: {e}")))
}
}
}
#[instrument(skip(self))]
async fn lsp_document_symbols(
&self,
client_name: &str,
document: DocumentIdentifier,
) -> Result<Option<DocumentSymbolResult>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let buffer_id = match &document {
DocumentIdentifier::BufferId(id) => *id,
_ => 0, };
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_document_symbols.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&DocumentSymbolParams { text_document }).unwrap(),
), Value::from(self.config.lsp_timeout_ms), Value::from(buffer_id), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<DocumentSymbolResult>>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse document symbols result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse document symbols result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get document symbols: {}", e);
Err(NeovimError::Api(format!(
"Failed to get document symbols: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_workspace_symbols(
&self,
client_name: &str,
query: &str,
) -> Result<Option<DocumentSymbolResult>, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_workspace_symbols.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&WorkspaceSymbolParams {
query: query.to_string(),
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<DocumentSymbolResult>>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse workspace symbols result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse workspace symbols result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get workspace symbols: {}", e);
Err(NeovimError::Api(format!(
"Failed to get workspace symbols: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_references(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
include_declaration: bool,
) -> Result<Vec<Location>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let buffer_id = match &document {
DocumentIdentifier::BufferId(id) => *id,
_ => 0, };
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_references.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&ReferenceParams {
text_document,
position,
context: ReferenceContext {
include_declaration,
},
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), Value::from(buffer_id), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<Vec<Location>>>>(
result.as_str().unwrap(),
) {
Ok(d) => {
let result: Result<Option<Vec<Location>>, NeovimError> = d.into();
result.map(|opt| opt.unwrap_or_default())
}
Err(e) => {
debug!("Failed to parse references result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse references result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get LSP references: {}", e);
Err(NeovimError::Api(format!(
"Failed to get LSP references: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_definition(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<LocateResult>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_definition.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&TextDocumentPositionParams {
text_document,
position,
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<LocateResult>>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse definition result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse definition result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get LSP definition: {}", e);
Err(NeovimError::Api(format!(
"Failed to get LSP definition: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_type_definition(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<LocateResult>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_type_definition.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&TextDocumentPositionParams {
text_document,
position,
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<LocateResult>>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse type definition result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse type definition result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get LSP type definition: {}", e);
Err(NeovimError::Api(format!(
"Failed to get LSP type definition: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_implementation(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<LocateResult>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_implementation.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&TextDocumentPositionParams {
text_document,
position,
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<LocateResult>>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse implementation result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse implementation result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get LSP implementation: {}", e);
Err(NeovimError::Api(format!(
"Failed to get LSP implementation: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_declaration(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<LocateResult>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_declaration.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&TextDocumentPositionParams {
text_document,
position,
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<LocateResult>>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse declaration result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse declaration result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get LSP declaration: {}", e);
Err(NeovimError::Api(format!(
"Failed to get LSP declaration: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_resolve_code_action(
&self,
client_name: &str,
code_action: CodeAction,
) -> Result<CodeAction, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_resolve_code_action.lua"),
vec![
Value::from(client_name),
Value::from(serde_json::to_string(&code_action).map_err(|e| {
NeovimError::Api(format!("Failed to serialize code action: {e}"))
})?),
Value::from(5000), Value::from(0), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<CodeAction>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse resolve code action result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse resolve code action result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to resolve LSP code action: {}", e);
Err(NeovimError::Api(format!(
"Failed to resolve LSP code action: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_apply_workspace_edit(
&self,
client_name: &str,
workspace_edit: WorkspaceEdit,
) -> Result<(), NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_apply_workspace_edit.lua"),
vec![
Value::from(client_name),
Value::from(serde_json::to_string(&workspace_edit).map_err(|e| {
NeovimError::Api(format!("Failed to serialize workspace edit: {e}"))
})?),
],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<()>>(result.as_str().unwrap()) {
Ok(rv) => rv.into(),
Err(e) => {
debug!("Failed to parse apply workspace edit result: {}", e);
Err(NeovimError::Api(format!(
"Failed to parse apply workspace edit result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to apply LSP workspace edit: {}", e);
Err(NeovimError::Api(format!(
"Failed to apply LSP workspace edit: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_prepare_rename(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<PrepareRenameResult>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let buffer_id = match &document {
DocumentIdentifier::BufferId(id) => *id,
_ => 0,
};
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_prepare_rename.lua"),
vec![
Value::from(client_name),
Value::from(
serde_json::to_string(&TextDocumentPositionParams {
text_document,
position,
})
.unwrap(),
),
Value::from(self.config.lsp_timeout_ms),
Value::from(buffer_id),
],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<PrepareRenameResult>>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse prepare rename result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse prepare rename result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to prepare rename: {}", e);
Err(NeovimError::Api(format!("Failed to prepare rename: {e}")))
}
}
}
#[instrument(skip(self))]
async fn lsp_rename(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
new_name: &str,
) -> Result<Option<WorkspaceEdit>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let buffer_id = match &document {
DocumentIdentifier::BufferId(id) => *id,
_ => 0,
};
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_rename.lua"),
vec![
Value::from(client_name),
Value::from(
serde_json::to_string(&RenameRequestParams {
text_document,
position,
new_name: new_name.to_string(),
})
.unwrap(),
),
Value::from(5000), Value::from(buffer_id),
],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<WorkspaceEdit>>>(
result.as_str().unwrap(),
) {
Ok(d) => d.into(),
Err(e) => {
debug!("Failed to parse rename result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse rename result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to rename: {}", e);
Err(NeovimError::Api(format!("Failed to rename: {e}")))
}
}
}
#[instrument(skip(self))]
async fn lsp_formatting(
&self,
client_name: &str,
document: DocumentIdentifier,
options: FormattingOptions,
) -> Result<Vec<TextEdit>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct DocumentFormattingRequest {
text_document: TextDocumentIdentifier,
options: FormattingOptions,
}
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_formatting.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&DocumentFormattingRequest {
text_document,
options,
})
.unwrap(),
),
Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<Vec<TextEdit>>>>(
result.as_str().unwrap(),
) {
Ok(d) => {
let rv: Result<Option<Vec<TextEdit>>, NeovimError> = d.into();
rv.map(|x| x.unwrap_or_default())
}
Err(e) => {
debug!("Failed to parse formatting result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse formatting result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to format document: {}", e);
Err(NeovimError::Api(format!("Failed to format document: {e}")))
}
}
}
#[instrument(skip(self))]
async fn lsp_range_formatting(
&self,
client_name: &str,
document: DocumentIdentifier,
range: Range,
options: FormattingOptions,
) -> Result<Vec<TextEdit>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct DocumentRangeFormattingRequest {
text_document: TextDocumentIdentifier,
range: Range,
options: FormattingOptions,
}
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_range_formatting.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&DocumentRangeFormattingRequest {
text_document,
range,
options,
})
.unwrap(),
),
Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<Vec<TextEdit>>>>(
result.as_str().unwrap(),
) {
Ok(d) => {
let rv: Result<Option<Vec<TextEdit>>, NeovimError> = d.into();
rv.map(|x| x.unwrap_or_default())
}
Err(e) => {
debug!("Failed to parse range formatting result: {e}");
Err(NeovimError::Api(format!(
"Failed to parse range formatting result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to format range: {}", e);
Err(NeovimError::Api(format!("Failed to format range: {e}")))
}
}
}
#[instrument(skip(self))]
async fn lsp_get_organize_imports_actions(
&self,
client_name: &str,
document: DocumentIdentifier,
) -> Result<Vec<CodeAction>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let range = Range::default();
let context = CodeActionContext {
diagnostics: Vec::new(), only: Some(vec![CodeActionKind::SourceOrganizeImports]),
trigger_kind: None,
};
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let buffer_id = match &document {
DocumentIdentifier::BufferId(id) => *id,
_ => 0, };
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_client_get_code_actions.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&CodeActionParams {
text_document,
range,
context,
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), Value::from(buffer_id), ],
)
.await
{
Ok(actions) => {
let actions = serde_json::from_str::<CodeActionResult>(actions.as_str().unwrap())
.map_err(|e| {
NeovimError::Api(format!("Failed to parse code actions: {e}"))
})?;
debug!("Found {} organize imports actions", actions.result.len());
Ok(actions.result)
}
Err(e) => {
debug!("Failed to get organize imports actions: {}", e);
Err(NeovimError::Api(format!(
"Failed to get organize imports actions: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_apply_text_edits(
&self,
client_name: &str,
document: DocumentIdentifier,
text_edits: Vec<TextEdit>,
) -> Result<(), NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_apply_text_edits.lua"),
vec![
Value::from(client_name),
Value::from(serde_json::to_string(&text_edits).map_err(|e| {
NeovimError::Api(format!("Failed to serialize text edits: {e}"))
})?),
Value::from(text_document.uri),
],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<()>>(result.as_str().unwrap()) {
Ok(rv) => rv.into(),
Err(e) => {
debug!("Failed to parse apply text edits result: {}", e);
Err(NeovimError::Api(format!(
"Failed to parse apply text edits result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to apply text edits: {}", e);
Err(NeovimError::Api(format!("Failed to apply text edits: {e}")))
}
}
}
#[instrument(skip(self))]
async fn navigate(
&self,
document: DocumentIdentifier,
position: Position,
) -> Result<NavigateResult, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/navigate.lua"),
vec![Value::from(
serde_json::to_string(&TextDocumentPositionParams {
text_document,
position,
})
.unwrap(),
)],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<NavigateResult>>(
result.as_str().unwrap(),
) {
Ok(rv) => rv.into(),
Err(e) => {
debug!("Failed to parse navigate result: {}", e);
Err(NeovimError::Api(format!(
"Failed to parse navigate result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to navigate: {}", e);
Err(NeovimError::Api(format!("Failed to navigate: {e}")))
}
}
}
#[instrument(skip(self))]
async fn wait_for_notification(
&self,
notification_name: &str,
timeout_ms: u64,
) -> Result<Notification, NeovimError> {
debug!(
"Waiting for notification: {} with timeout: {}ms",
notification_name, timeout_ms
);
let tracker = self.notification_tracker.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
tracker
.wait_for_notification(notification_name, Duration::from_millis(timeout_ms))
.await
}
#[instrument(skip(self))]
async fn wait_for_lsp_ready(
&self,
client_name: Option<&str>,
timeout_ms: u64,
) -> Result<(), NeovimError> {
debug!(
"Waiting for LSP client readiness: {:?} with timeout: {}ms",
client_name, timeout_ms
);
let notification = self
.wait_for_notification("NVIM_MCP_LspAttach", timeout_ms)
.await?;
if let Some(expected_client_name) = client_name {
if let Some(attach_data) = notification.args.first() {
if let Value::Map(map) = attach_data {
let client_name_key = Value::String("client_name".into());
let client_name_value = map
.iter()
.find(|(k, _)| k == &client_name_key)
.map(|(_, v)| v);
if let Some(Value::String(actual_client_name)) = client_name_value {
let actual_str = actual_client_name.as_str().unwrap_or("");
if actual_str != expected_client_name {
return Err(NeovimError::Api(format!(
"LSP client '{}' attached but expected '{}'",
actual_str, expected_client_name
)));
}
} else {
return Err(NeovimError::Api(
"LSP attach notification missing or invalid client_name".to_string(),
));
}
} else {
return Err(NeovimError::Api(
"LSP attach notification data is not a map".to_string(),
));
}
} else {
return Err(NeovimError::Api(
"LSP attach notification missing data".to_string(),
));
}
}
debug!("LSP client readiness confirmed");
Ok(())
}
#[instrument(skip(self))]
async fn wait_for_diagnostics(
&self,
buffer_id: Option<u64>,
timeout_ms: u64,
) -> Result<Vec<Diagnostic>, NeovimError> {
debug!(
"Waiting for diagnostics for buffer {:?} with timeout: {}ms",
buffer_id, timeout_ms
);
match self.get_diagnostics(buffer_id).await {
Ok(diagnostics) if !diagnostics.is_empty() => {
debug!("Found {} diagnostics immediately", diagnostics.len());
return Ok(diagnostics);
}
Ok(_) => {
debug!("No diagnostics found, waiting for notification");
}
Err(e) => {
debug!("Error getting diagnostics: {}, waiting for notification", e);
}
}
let notification = self
.wait_for_notification("NVIM_MCP_DiagnosticsChanged", timeout_ms)
.await?;
debug!("Received diagnostics notification: {:?}", notification);
self.get_diagnostics(buffer_id).await
}
#[instrument(skip(self))]
async fn lsp_call_hierarchy_prepare(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<Vec<CallHierarchyItem>>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_call_hierarchy_prepare.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&CallHierarchyPrepareParams {
text_document,
position,
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<Vec<CallHierarchyItem>>>>(
result.as_str().unwrap(),
) {
Ok(rv) => rv.into(),
Err(e) => {
debug!("Failed to parse call hierarchy prepare result: {}", e);
Err(NeovimError::Api(format!(
"Failed to parse call hierarchy prepare result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to prepare call hierarchy: {}", e);
Err(NeovimError::Api(format!(
"Failed to prepare call hierarchy: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_call_hierarchy_incoming_calls(
&self,
client_name: &str,
item: CallHierarchyItem,
) -> Result<Option<Vec<CallHierarchyIncomingCall>>, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_call_hierarchy_incoming_calls.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&CallHierarchyIncomingCallsParams { item }).unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<
NvimExecuteLuaResult<Option<Vec<CallHierarchyIncomingCall>>>,
>(result.as_str().unwrap())
{
Ok(rv) => rv.into(),
Err(e) => {
debug!(
"Failed to parse call hierarchy incoming calls result: {}",
e
);
Err(NeovimError::Api(format!(
"Failed to parse call hierarchy incoming calls result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get call hierarchy incoming calls: {}", e);
Err(NeovimError::Api(format!(
"Failed to get call hierarchy incoming calls: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_call_hierarchy_outgoing_calls(
&self,
client_name: &str,
item: CallHierarchyItem,
) -> Result<Option<Vec<CallHierarchyOutgoingCall>>, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_call_hierarchy_outgoing_calls.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&CallHierarchyOutgoingCallsParams { item }).unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<
NvimExecuteLuaResult<Option<Vec<CallHierarchyOutgoingCall>>>,
>(result.as_str().unwrap())
{
Ok(rv) => rv.into(),
Err(e) => {
debug!(
"Failed to parse call hierarchy outgoing calls result: {}",
e
);
Err(NeovimError::Api(format!(
"Failed to parse call hierarchy outgoing calls result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get call hierarchy outgoing calls: {}", e);
Err(NeovimError::Api(format!(
"Failed to get call hierarchy outgoing calls: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_type_hierarchy_prepare(
&self,
client_name: &str,
document: DocumentIdentifier,
position: Position,
) -> Result<Option<Vec<TypeHierarchyItem>>, NeovimError> {
let text_document = self.resolve_text_document_identifier(&document).await?;
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_type_hierarchy_prepare.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&TypeHierarchyPrepareParams {
text_document,
position,
})
.unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<Vec<TypeHierarchyItem>>>>(
result.as_str().unwrap(),
) {
Ok(rv) => rv.into(),
Err(e) => {
debug!("Failed to parse type hierarchy prepare result: {}", e);
Err(NeovimError::Api(format!(
"Failed to parse type hierarchy prepare result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to prepare type hierarchy: {}", e);
Err(NeovimError::Api(format!(
"Failed to prepare type hierarchy: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_type_hierarchy_supertypes(
&self,
client_name: &str,
item: TypeHierarchyItem,
) -> Result<Option<Vec<TypeHierarchyItem>>, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_type_hierarchy_supertypes.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&TypeHierarchySupertypesParams { item }).unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<Vec<TypeHierarchyItem>>>>(
result.as_str().unwrap(),
) {
Ok(rv) => rv.into(),
Err(e) => {
debug!("Failed to parse type hierarchy supertypes result: {}", e);
Err(NeovimError::Api(format!(
"Failed to parse type hierarchy supertypes result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get type hierarchy supertypes: {}", e);
Err(NeovimError::Api(format!(
"Failed to get type hierarchy supertypes: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn lsp_type_hierarchy_subtypes(
&self,
client_name: &str,
item: TypeHierarchyItem,
) -> Result<Option<Vec<TypeHierarchyItem>>, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
match conn
.nvim
.execute_lua(
include_str!("lua/lsp_type_hierarchy_subtypes.lua"),
vec![
Value::from(client_name), Value::from(
serde_json::to_string(&TypeHierarchySubtypesParams { item }).unwrap(),
), Value::from(self.config.lsp_timeout_ms), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<Option<Vec<TypeHierarchyItem>>>>(
result.as_str().unwrap(),
) {
Ok(rv) => rv.into(),
Err(e) => {
debug!("Failed to parse type hierarchy subtypes result: {}", e);
Err(NeovimError::Api(format!(
"Failed to parse type hierarchy subtypes result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get type hierarchy subtypes: {}", e);
Err(NeovimError::Api(format!(
"Failed to get type hierarchy subtypes: {e}"
)))
}
}
}
#[instrument(skip(self))]
async fn read_document(
&self,
document: DocumentIdentifier,
start: i64,
end: i64,
) -> Result<String, NeovimError> {
let conn = self.connection.as_ref().ok_or_else(|| {
NeovimError::Connection("Not connected to any Neovim instance".to_string())
})?;
let params = match &document {
DocumentIdentifier::BufferId(buffer_id) => {
ReadDocumentParams::buffer_id(*buffer_id, start, end)
}
DocumentIdentifier::ProjectRelativePath(rel_path) => {
let project_root = self.get_project_root().await?;
let absolute_path = project_root.join(rel_path);
ReadDocumentParams::path(absolute_path, start, end)
}
DocumentIdentifier::AbsolutePath(abs_path) => {
ReadDocumentParams::path(abs_path, start, end)
}
};
match conn
.nvim
.execute_lua(
include_str!("lua/read_document.lua"),
vec![
Value::from(serde_json::to_string(¶ms).unwrap()), ],
)
.await
{
Ok(result) => {
match serde_json::from_str::<NvimExecuteLuaResult<String>>(result.as_str().unwrap())
{
Ok(rv) => rv.into(),
Err(e) => {
debug!("Failed to parse read document result: {}", e);
Err(NeovimError::Api(format!(
"Failed to parse read document result: {e}"
)))
}
}
}
Err(e) => {
debug!("Failed to get read document: {}", e);
Err(NeovimError::Api(format!(
"Failed to get read document: {e}"
)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
#[test]
fn test_symbol_kind_serialization() {
assert_eq!(serde_json::to_value(SymbolKind::Function).unwrap(), 12);
assert_eq!(serde_json::to_value(SymbolKind::Variable).unwrap(), 13);
assert_eq!(serde_json::to_value(SymbolKind::Class).unwrap(), 5);
}
#[test]
fn test_symbol_information_serialization() {
let symbol = SymbolInformation {
name: "test_function".to_string(),
kind: SymbolKind::Function,
tags: None,
deprecated: None,
location: Location {
uri: "file:///test.rs".to_string(),
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 13,
},
},
},
container_name: None,
};
let json = serde_json::to_string(&symbol).unwrap();
assert!(json.contains("test_function"));
assert!(json.contains("file:///test.rs"));
}
#[test]
fn test_document_symbol_serialization() {
let symbol = DocumentSymbol {
name: "TestClass".to_string(),
detail: Some("class TestClass".to_string()),
kind: SymbolKind::Class,
tags: None,
deprecated: None,
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 10,
character: 0,
},
},
selection_range: Range {
start: Position {
line: 0,
character: 6,
},
end: Position {
line: 0,
character: 15,
},
},
children: None,
};
let json = serde_json::to_string(&symbol).unwrap();
assert!(json.contains("TestClass"));
assert!(json.contains("class TestClass"));
}
#[test]
fn test_workspace_symbol_params_serialization() {
let params = WorkspaceSymbolParams {
query: "function".to_string(),
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("function"));
}
#[test]
fn test_reference_params_serialization() {
let params = ReferenceParams {
text_document: TextDocumentIdentifier {
uri: "file:///test.rs".to_string(),
version: Some(1),
},
position: Position {
line: 10,
character: 5,
},
context: ReferenceContext {
include_declaration: true,
},
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("textDocument"));
assert!(json.contains("position"));
assert!(json.contains("context"));
assert!(json.contains("includeDeclaration"));
assert!(json.contains("true"));
}
#[derive(serde::Deserialize)]
struct DocumentIdentifierWrapper {
#[serde(deserialize_with = "string_or_struct")]
pub identifier: DocumentIdentifier,
}
#[test]
fn test_make_text_document_identifier_from_path() {
let current_file = file!();
let result = make_text_document_identifier_from_path(current_file);
assert!(result.is_ok());
let text_doc = result.unwrap();
assert!(text_doc.uri.starts_with("file://"));
assert!(text_doc.uri.ends_with("client.rs"));
assert_eq!(text_doc.version, None);
}
#[test]
fn test_make_text_document_identifier_from_path_invalid() {
let result = make_text_document_identifier_from_path("/nonexistent/path/file.rs");
assert!(result.is_err());
if let Err(NeovimError::Api(msg)) = result {
assert!(msg.contains("Failed to resolve path"));
} else {
panic!("Expected NeovimError::Api");
}
}
#[test]
fn test_document_identifier_creation() {
let buffer_id = DocumentIdentifier::from_buffer_id(42);
assert_eq!(buffer_id, DocumentIdentifier::BufferId(42));
let rel_path = DocumentIdentifier::from_project_path("src/lib.rs");
assert_eq!(
rel_path,
DocumentIdentifier::ProjectRelativePath(PathBuf::from("src/lib.rs"))
);
let abs_path = DocumentIdentifier::from_absolute_path("/usr/src/lib.rs");
assert_eq!(
abs_path,
DocumentIdentifier::AbsolutePath(PathBuf::from("/usr/src/lib.rs"))
);
}
#[test]
fn test_document_identifier_serde() {
let buffer_id = DocumentIdentifier::from_buffer_id(123);
let json = serde_json::to_string(&buffer_id).unwrap();
assert!(json.contains("buffer_id"));
assert!(json.contains("123"));
let deserialized: DocumentIdentifier = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, buffer_id);
let rel_path = DocumentIdentifier::from_project_path("src/main.rs");
let json = serde_json::to_string(&rel_path).unwrap();
assert!(json.contains("project_relative_path"));
assert!(json.contains("src/main.rs"));
let deserialized: DocumentIdentifier = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, rel_path);
let abs_path = DocumentIdentifier::from_absolute_path("/tmp/test.rs");
let json = serde_json::to_string(&abs_path).unwrap();
assert!(json.contains("absolute_path"));
assert!(json.contains("/tmp/test.rs"));
let deserialized: DocumentIdentifier = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, abs_path);
}
#[test]
fn test_document_identifier_json_schema() {
use schemars::schema_for;
let schema = schema_for!(DocumentIdentifier);
let schema_json = serde_json::to_string(&schema).unwrap();
assert!(schema_json.contains("buffer_id"));
assert!(schema_json.contains("project_relative_path"));
assert!(schema_json.contains("absolute_path"));
}
#[test]
fn test_document_identifier_string_deserializer_buffer_id() {
let json_str = r#"{"buffer_id": 42}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(result.unwrap(), DocumentIdentifier::BufferId(42));
let json_str = r#"{"buffer_id": 18446744073709551615}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
DocumentIdentifier::BufferId(18446744073709551615)
);
}
#[test]
fn test_document_identifier_string_deserializer_project_path() {
let json_str = r#"{"project_relative_path": "src/main.rs"}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
DocumentIdentifier::ProjectRelativePath(PathBuf::from("src/main.rs"))
);
let json_str = r#"{"project_relative_path": "src/server/tools.rs"}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
DocumentIdentifier::ProjectRelativePath(PathBuf::from("src/server/tools.rs"))
);
let json_str = r#"{"project_relative_path": ""}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
DocumentIdentifier::ProjectRelativePath(PathBuf::from(""))
);
}
#[test]
fn test_document_identifier_string_deserializer_absolute_path() {
let json_str = r#"{"absolute_path": "/usr/local/src/main.rs"}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
DocumentIdentifier::AbsolutePath(PathBuf::from("/usr/local/src/main.rs"))
);
let json_str = r#"{"absolute_path": "C:\\Users\\test\\Documents\\file.rs"}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
DocumentIdentifier::AbsolutePath(PathBuf::from("C:\\Users\\test\\Documents\\file.rs"))
);
}
#[test]
fn test_document_identifier_string_deserializer_error_cases() {
let invalid_json = r#"{"invalid_field": 42}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(invalid_json);
assert!(result.is_err());
let empty_json = r#"{}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(empty_json);
assert!(result.is_err());
let malformed_json = r#"{"buffer_id": invalid}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(malformed_json);
assert!(result.is_err());
let negative_json = r#"{"buffer_id": -1}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(negative_json);
assert!(result.is_err());
let malformed_embedded = r#""{\"buffer_id\": }"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(malformed_embedded);
assert!(result.is_err());
}
#[test]
fn test_document_identifier_string_deserializer_mixed_cases() {
let json_str = r#"{ "buffer_id" : 999 }"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(result.unwrap(), DocumentIdentifier::BufferId(999));
let json_str = r#"{"project_relative_path": "src/测试.rs"}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
DocumentIdentifier::ProjectRelativePath(PathBuf::from("src/测试.rs"))
);
let json_str = r#"{"absolute_path": "/tmp/test with spaces & symbols!.rs"}"#;
let result: Result<DocumentIdentifier, _> = serde_json::from_str(json_str);
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
DocumentIdentifier::AbsolutePath(PathBuf::from("/tmp/test with spaces & symbols!.rs"))
);
}
#[test]
fn test_document_identifier_round_trip_serialization() {
let test_cases = vec![
DocumentIdentifier::BufferId(42),
DocumentIdentifier::ProjectRelativePath(PathBuf::from("src/main.rs")),
DocumentIdentifier::AbsolutePath(PathBuf::from("/usr/src/main.rs")),
];
for original in test_cases {
let json = serde_json::to_string(&original).unwrap();
let deserialized: DocumentIdentifier = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
let json_string = serde_json::to_string(&serde_json::json!( {
"identifier": json,
}))
.unwrap();
let from_string: DocumentIdentifierWrapper =
serde_json::from_str(&json_string).unwrap();
assert_eq!(original, from_string.identifier);
}
}
#[test]
fn test_code_action_serialization() {
let code_action = CodeAction {
title: "Fix this issue".to_string(),
kind: Some(CodeActionKind::Quickfix),
diagnostics: Some(vec![]),
is_preferred: Some(true),
disabled: None,
edit: Some(WorkspaceEdit {
changes: Some(std::collections::HashMap::new()),
document_changes: None,
change_annotations: None,
}),
command: None,
data: None,
};
let json = serde_json::to_string(&code_action).unwrap();
assert!(json.contains("Fix this issue"));
assert!(json.contains("quickfix"));
let deserialized: CodeAction = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.title, "Fix this issue");
assert_eq!(deserialized.kind, Some(CodeActionKind::Quickfix));
}
#[test]
fn test_workspace_edit_serialization() {
let mut changes = std::collections::HashMap::new();
changes.insert(
"file:///test.rs".to_string(),
vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
new_text: "hello".to_string(),
annotation_id: None,
}],
);
let workspace_edit = WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
};
let json = serde_json::to_string(&workspace_edit).unwrap();
assert!(json.contains("file:///test.rs"));
assert!(json.contains("hello"));
let deserialized: WorkspaceEdit = serde_json::from_str(&json).unwrap();
assert!(deserialized.changes.is_some());
}
#[derive(serde::Deserialize)]
struct CodeActionWrapper {
#[serde(deserialize_with = "string_or_struct")]
pub code_action: CodeAction,
}
#[test]
fn test_code_action_string_deserialization() {
let code_action = CodeAction {
title: "Fix this issue".to_string(),
kind: Some(CodeActionKind::Quickfix),
diagnostics: None,
is_preferred: Some(true),
disabled: None,
edit: None,
command: None,
data: None,
};
let json = serde_json::to_string(&code_action).unwrap();
let deserialized_object: CodeAction = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized_object.title, "Fix this issue");
assert_eq!(deserialized_object.kind, Some(CodeActionKind::Quickfix));
let json_string = serde_json::json!({
"code_action": json
});
let deserialized: CodeActionWrapper = serde_json::from_value(json_string).unwrap();
let deserialized = deserialized.code_action;
assert_eq!(deserialized.title, "Fix this issue");
assert_eq!(deserialized.kind, Some(CodeActionKind::Quickfix));
}
#[derive(serde::Deserialize)]
struct WorkspaceEditWrapper {
#[serde(deserialize_with = "string_or_struct")]
pub workspace_edit: WorkspaceEdit,
}
#[test]
fn test_workspace_edit_string_deserialization() {
let mut changes = std::collections::HashMap::new();
changes.insert(
"file:///test.rs".to_string(),
vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
new_text: "hello".to_string(),
annotation_id: None,
}],
);
let workspace_edit = WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
};
let json = serde_json::to_string(&workspace_edit).unwrap();
let deserialized_object: WorkspaceEdit = serde_json::from_str(&json).unwrap();
assert!(deserialized_object.changes.is_some());
let json_string = serde_json::json!({
"workspace_edit": json
});
let deserialized: WorkspaceEditWrapper = serde_json::from_value(json_string).unwrap();
let deserialized = deserialized.workspace_edit;
assert!(deserialized.changes.is_some());
}
#[tokio::test]
async fn test_notification_tracker_basic() {
let tracker = NotificationTracker::default();
tracker
.record_notification(
"test_notification".to_string(),
vec![Value::from("test_arg")],
)
.await;
let result = tracker
.wait_for_notification("test_notification", Duration::from_millis(100))
.await;
assert!(result.is_ok());
let notification = result.unwrap();
assert_eq!(notification.name, "test_notification");
assert_eq!(notification.args.len(), 1);
assert_eq!(notification.args[0].as_str().unwrap(), "test_arg");
}
#[tokio::test]
async fn test_notification_tracker_timeout() {
let tracker = NotificationTracker::default();
let result = tracker
.wait_for_notification("nonexistent_notification", Duration::from_millis(50))
.await;
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, NeovimError::Api(_)));
assert!(
error
.to_string()
.contains("Timeout waiting for notification")
);
}
#[tokio::test]
async fn test_notification_tracker_wait_then_send() {
let tracker = NotificationTracker::default();
let wait_handle = tokio::spawn({
let tracker = tracker.clone();
async move {
tracker
.wait_for_notification("test_async_notification", Duration::from_millis(500))
.await
}
});
tokio::time::sleep(Duration::from_millis(10)).await;
tracker
.record_notification(
"test_async_notification".to_string(),
vec![Value::from("async_test_arg")],
)
.await;
let result = wait_handle.await.unwrap();
assert!(result.is_ok());
let notification = result.unwrap();
assert_eq!(notification.name, "test_async_notification");
assert_eq!(notification.args.len(), 1);
assert_eq!(notification.args[0].as_str().unwrap(), "async_test_arg");
}
#[tokio::test]
async fn test_notification_cleanup_expired() {
let tracker = NotificationTracker::default();
let old_notification = Notification {
name: "old_notification".to_string(),
args: vec![Value::from("old_data")],
timestamp: std::time::SystemTime::now()
- Duration::from_secs(NOTIFICATION_EXPIRY_SECONDS + 1),
};
{
let mut notifications = tracker.notifications.lock().await;
notifications.push(old_notification);
}
tracker
.record_notification(
"fresh_notification".to_string(),
vec![Value::from("fresh_data")],
)
.await;
let (count_before, _) = tracker.get_stats().await;
assert_eq!(count_before, 2);
tracker.cleanup_expired_notifications().await;
let (count_after, _) = tracker.get_stats().await;
assert_eq!(count_after, 1);
let result = tracker
.wait_for_notification("fresh_notification", Duration::from_millis(10))
.await;
assert!(result.is_ok());
let result = tracker
.wait_for_notification("old_notification", Duration::from_millis(10))
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_notification_cleanup_excess() {
let tracker = NotificationTracker::default();
for i in 0..(MAX_STORED_NOTIFICATIONS + 10) {
tracker
.record_notification(format!("notification_{}", i), vec![Value::from(i as i64)])
.await;
}
let (count, _) = tracker.get_stats().await;
assert!(count <= MAX_STORED_NOTIFICATIONS);
let result = tracker
.wait_for_notification(
&format!("notification_{}", MAX_STORED_NOTIFICATIONS + 9),
Duration::from_millis(10),
)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_notification_expiry_in_wait() {
let tracker = NotificationTracker::default();
let expired_notification = Notification {
name: "expired_test".to_string(),
args: vec![Value::from("expired_data")],
timestamp: std::time::SystemTime::now()
- Duration::from_secs(NOTIFICATION_EXPIRY_SECONDS + 1),
};
{
let mut notifications = tracker.notifications.lock().await;
notifications.push(expired_notification);
}
let result = tracker
.wait_for_notification("expired_test", Duration::from_millis(50))
.await;
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Timeout waiting for notification")
);
}
}