use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::lsp::ops::{LspClientOps, LspProvider};
use crate::lsp::SymbolInfo;
pub struct MockLspClient {
symbols: HashMap<PathBuf, Vec<SymbolInfo>>,
symbols_sequence:
std::sync::Mutex<HashMap<PathBuf, std::collections::VecDeque<Vec<SymbolInfo>>>>,
definitions: HashMap<(u32, u32), Vec<lsp_types::Location>>,
workspace_results: Vec<SymbolInfo>,
hover_responses: std::sync::Mutex<std::collections::VecDeque<anyhow::Result<Option<String>>>>,
goto_responses:
std::sync::Mutex<std::collections::VecDeque<anyhow::Result<Vec<lsp_types::Location>>>>,
pub prepare_call_hierarchy_results: std::sync::Mutex<
std::collections::HashMap<
(std::path::PathBuf, u32, u32),
Option<lsp_types::CallHierarchyItem>,
>,
>,
pub incoming_calls_results: std::sync::Mutex<
std::collections::HashMap<String, Vec<lsp_types::CallHierarchyIncomingCall>>,
>,
pub outgoing_calls_results: std::sync::Mutex<
std::collections::HashMap<String, Vec<lsp_types::CallHierarchyOutgoingCall>>,
>,
pub references_results:
std::sync::Mutex<std::collections::HashMap<std::path::PathBuf, Vec<lsp_types::Location>>>,
}
impl MockLspClient {
pub fn new() -> Self {
Self {
symbols: HashMap::new(),
symbols_sequence: std::sync::Mutex::new(HashMap::new()),
definitions: HashMap::new(),
workspace_results: vec![],
hover_responses: std::sync::Mutex::new(std::collections::VecDeque::new()),
goto_responses: std::sync::Mutex::new(std::collections::VecDeque::new()),
prepare_call_hierarchy_results: std::sync::Mutex::new(std::collections::HashMap::new()),
incoming_calls_results: std::sync::Mutex::new(std::collections::HashMap::new()),
outgoing_calls_results: std::sync::Mutex::new(std::collections::HashMap::new()),
references_results: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
pub fn with_symbols(mut self, path: impl Into<PathBuf>, syms: Vec<SymbolInfo>) -> Self {
self.symbols.insert(path.into(), syms);
self
}
pub fn with_symbols_sequence(
self,
path: impl Into<PathBuf>,
sequence: Vec<Vec<SymbolInfo>>,
) -> Self {
self.symbols_sequence
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(path.into(), sequence.into());
self
}
pub fn with_definitions(
mut self,
line: u32,
col: u32,
locations: Vec<lsp_types::Location>,
) -> Self {
self.definitions.insert((line, col), locations);
self
}
pub fn with_workspace_symbols(mut self, syms: Vec<SymbolInfo>) -> Self {
self.workspace_results = syms;
self
}
pub fn with_hover_responses(self, responses: Vec<anyhow::Result<Option<String>>>) -> Self {
let mut q = self
.hover_responses
.lock()
.unwrap_or_else(|e| e.into_inner());
q.extend(responses);
drop(q);
self
}
pub fn with_goto_responses(
self,
responses: Vec<anyhow::Result<Vec<lsp_types::Location>>>,
) -> Self {
let mut q = self
.goto_responses
.lock()
.unwrap_or_else(|e| e.into_inner());
q.extend(responses);
drop(q);
self
}
}
impl Default for MockLspClient {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl LspClientOps for MockLspClient {
async fn document_symbols(
&self,
path: &Path,
_language_id: &str,
) -> anyhow::Result<Vec<SymbolInfo>> {
if let Some(front) = self
.symbols_sequence
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(path)
.and_then(|q| q.front().cloned())
{
return Ok(front);
}
Ok(self.symbols.get(path).cloned().unwrap_or_default())
}
async fn workspace_symbols(&self, _query: &str) -> anyhow::Result<Vec<SymbolInfo>> {
Ok(self.workspace_results.clone())
}
async fn references(
&self,
path: &Path,
_line: u32,
_col: u32,
_language_id: &str,
) -> anyhow::Result<Vec<lsp_types::Location>> {
let map = self
.references_results
.lock()
.unwrap_or_else(|e| e.into_inner());
Ok(map.get(path).cloned().unwrap_or_default())
}
async fn goto_definition(
&self,
_path: &Path,
line: u32,
col: u32,
_language_id: &str,
) -> anyhow::Result<Vec<lsp_types::Location>> {
if let Some(next) = self
.goto_responses
.lock()
.unwrap_or_else(|e| e.into_inner())
.pop_front()
{
return next;
}
Ok(self
.definitions
.get(&(line, col))
.cloned()
.unwrap_or_default())
}
async fn hover(
&self,
_path: &Path,
_line: u32,
_col: u32,
_language_id: &str,
) -> anyhow::Result<Option<String>> {
if let Some(next) = self
.hover_responses
.lock()
.unwrap_or_else(|e| e.into_inner())
.pop_front()
{
return next;
}
Ok(None)
}
async fn rename(
&self,
_path: &Path,
_line: u32,
_col: u32,
_new_name: &str,
_language_id: &str,
) -> anyhow::Result<lsp_types::WorkspaceEdit> {
Ok(lsp_types::WorkspaceEdit::default())
}
async fn did_change(&self, path: &Path) -> anyhow::Result<()> {
if let Some(q) = self
.symbols_sequence
.lock()
.unwrap_or_else(|e| e.into_inner())
.get_mut(path)
{
if q.len() > 1 {
q.pop_front();
}
}
Ok(())
}
async fn prepare_call_hierarchy(
&self,
path: &Path,
line: u32,
col: u32,
_language_id: &str,
) -> anyhow::Result<Option<lsp_types::CallHierarchyItem>> {
Ok(self
.prepare_call_hierarchy_results
.lock()
.unwrap()
.get(&(path.to_path_buf(), line, col))
.cloned()
.flatten())
}
async fn incoming_calls(
&self,
item: &lsp_types::CallHierarchyItem,
_language_id: &str,
) -> anyhow::Result<Vec<lsp_types::CallHierarchyIncomingCall>> {
Ok(self
.incoming_calls_results
.lock()
.unwrap()
.get(&item.name)
.cloned()
.unwrap_or_default())
}
async fn outgoing_calls(
&self,
item: &lsp_types::CallHierarchyItem,
_language_id: &str,
) -> anyhow::Result<Vec<lsp_types::CallHierarchyOutgoingCall>> {
Ok(self
.outgoing_calls_results
.lock()
.unwrap()
.get(&item.name)
.cloned()
.unwrap_or_default())
}
}
pub struct MockLspProvider {
client: Arc<MockLspClient>,
}
impl MockLspProvider {
pub fn with_client(client: MockLspClient) -> Arc<dyn LspProvider> {
Arc::new(Self {
client: Arc::new(client),
})
}
}
#[async_trait::async_trait]
impl LspProvider for MockLspProvider {
async fn get_or_start(
&self,
_language: &str,
_workspace_root: &Path,
_mux_override: Option<bool>,
) -> anyhow::Result<Arc<dyn LspClientOps>> {
Ok(Arc::clone(&self.client) as Arc<dyn LspClientOps>)
}
async fn notify_file_changed(&self, _path: &Path) {}
async fn shutdown_all(&self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp::ops::LspClientOps;
#[tokio::test]
async fn mock_call_hierarchy_returns_canned_responses() {
let mock = MockLspClient::new();
let uri: lsp_types::Uri = "file:///a.rs".parse().unwrap();
let item = lsp_types::CallHierarchyItem {
name: "a".into(),
kind: lsp_types::SymbolKind::FUNCTION,
tags: None,
detail: None,
uri,
range: lsp_types::Range::default(),
selection_range: lsp_types::Range::default(),
data: None,
};
mock.prepare_call_hierarchy_results
.lock()
.unwrap()
.insert((std::path::PathBuf::from("a.rs"), 0, 0), Some(item.clone()));
let got = mock
.prepare_call_hierarchy(std::path::Path::new("a.rs"), 0, 0, "rust")
.await
.unwrap();
assert_eq!(got.unwrap().name, "a");
let miss = mock
.prepare_call_hierarchy(std::path::Path::new("a.rs"), 1, 1, "rust")
.await
.unwrap();
assert!(miss.is_none());
assert!(mock.incoming_calls(&item, "rust").await.unwrap().is_empty());
assert!(mock.outgoing_calls(&item, "rust").await.unwrap().is_empty());
let caller_uri: lsp_types::Uri = "file:///b.rs".parse().unwrap();
let caller = lsp_types::CallHierarchyItem {
name: "b".into(),
kind: lsp_types::SymbolKind::FUNCTION,
tags: None,
detail: None,
uri: caller_uri,
range: lsp_types::Range::default(),
selection_range: lsp_types::Range::default(),
data: None,
};
let incoming = lsp_types::CallHierarchyIncomingCall {
from: caller,
from_ranges: vec![],
};
mock.incoming_calls_results
.lock()
.unwrap()
.insert("a".into(), vec![incoming]);
let calls = mock.incoming_calls(&item, "rust").await.unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].from.name, "b");
}
}