use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::Arc;
use tokio::process::{Child, Command};
use tokio::sync::RwLock;
use url::Url;
use super::config::LspServerConfig;
use super::types::{
LspError, LspDiagnostic, Position, Range, Location,
CompletionItem, Hover, CodeAction, SymbolInformation
};
use super::LspService;
use async_trait::async_trait;
pub struct LspClient {
config: LspServerConfig,
process: Option<Child>,
diagnostics: Arc<RwLock<HashMap<PathBuf, Vec<LspDiagnostic>>>>,
open_files: Arc<RwLock<HashMap<PathBuf, String>>>,
initialized: Arc<RwLock<bool>>,
workspace_root: Option<PathBuf>,
}
impl LspClient {
pub fn new(config: LspServerConfig) -> Self {
Self {
config,
process: None,
diagnostics: Arc::new(RwLock::new(HashMap::new())),
open_files: Arc::new(RwLock::new(HashMap::new())),
initialized: Arc::new(RwLock::new(false)),
workspace_root: None,
}
}
pub async fn start(&mut self, workspace_root: Option<PathBuf>) -> Result<(), LspError> {
if self.process.is_some() {
return Ok(()); }
self.workspace_root = workspace_root;
let mut cmd = Command::new(&self.config.command);
cmd.args(&self.config.args);
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
for (key, value) in &self.config.env {
cmd.env(key, value);
}
let child = cmd.spawn()
.map_err(|e| LspError::ServerStartFailed(format!("Failed to start {}: {}", self.config.command, e)))?;
self.process = Some(child);
self.initialize().await?;
Ok(())
}
async fn initialize(&mut self) -> Result<(), LspError> {
let mut initialized = self.initialized.write().await;
*initialized = true;
tracing::info!("LSP server {} initialized", self.config.name);
Ok(())
}
pub async fn stop(&mut self) -> Result<(), LspError> {
if let Some(mut process) = self.process.take() {
if let Err(e) = process.kill().await {
tracing::warn!("Failed to kill LSP process: {}", e);
}
}
let mut initialized = self.initialized.write().await;
*initialized = false;
Ok(())
}
pub async fn is_initialized(&self) -> bool {
*self.initialized.read().await
}
pub async fn did_open(&self, file_path: &Path, content: &str) -> Result<(), LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
let mut open_files = self.open_files.write().await;
open_files.insert(file_path.to_path_buf(), content.to_string());
tracing::debug!("File opened: {}", file_path.display());
Ok(())
}
pub async fn did_change(&self, file_path: &Path, content: &str) -> Result<(), LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
let mut open_files = self.open_files.write().await;
open_files.insert(file_path.to_path_buf(), content.to_string());
tracing::debug!("File changed: {}", file_path.display());
Ok(())
}
pub async fn did_close(&self, file_path: &Path) -> Result<(), LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
let mut open_files = self.open_files.write().await;
open_files.remove(file_path);
let mut diagnostics = self.diagnostics.write().await;
diagnostics.remove(file_path);
tracing::debug!("File closed: {}", file_path.display());
Ok(())
}
pub async fn get_diagnostics(&self, file_path: &Path) -> Result<Vec<LspDiagnostic>, LspError> {
let diagnostics = self.diagnostics.read().await;
Ok(diagnostics.get(file_path).cloned().unwrap_or_default())
}
pub async fn get_all_diagnostics(&self) -> Result<Vec<LspDiagnostic>, LspError> {
let diagnostics = self.diagnostics.read().await;
let mut all_diagnostics = Vec::new();
for diags in diagnostics.values() {
all_diagnostics.extend(diags.clone());
}
Ok(all_diagnostics)
}
pub fn handles_file(&self, file_path: &Path) -> bool {
self.config.handles_file(file_path)
}
pub fn config(&self) -> &LspServerConfig {
&self.config
}
pub async fn get_completions(&self, file_path: &Path, position: Position) -> Result<Vec<CompletionItem>, LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
tracing::debug!("Getting completions for {} at {}:{}", file_path.display(), position.line, position.character);
Ok(Vec::new())
}
pub async fn goto_definition(&self, file_path: &Path, position: Position) -> Result<Vec<Location>, LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
tracing::debug!("Going to definition for {} at {}:{}", file_path.display(), position.line, position.character);
Ok(Vec::new())
}
pub async fn get_hover(&self, file_path: &Path, position: Position) -> Result<Option<Hover>, LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
tracing::debug!("Getting hover for {} at {}:{}", file_path.display(), position.line, position.character);
Ok(None)
}
pub async fn find_references(&self, file_path: &Path, position: Position, include_declaration: bool) -> Result<Vec<Location>, LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
tracing::debug!("Finding references for {} at {}:{} (include_declaration: {})",
file_path.display(), position.line, position.character, include_declaration);
Ok(Vec::new())
}
pub async fn get_code_actions(&self, file_path: &Path, range: Range) -> Result<Vec<CodeAction>, LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
tracing::debug!("Getting code actions for {} at {}:{} to {}:{}",
file_path.display(), range.start.line, range.start.character,
range.end.line, range.end.character);
Ok(Vec::new())
}
pub async fn get_document_symbols(&self, file_path: &Path) -> Result<Vec<SymbolInformation>, LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
tracing::debug!("Getting document symbols for {}", file_path.display());
Ok(Vec::new())
}
pub async fn format_document(&self, file_path: &Path) -> Result<String, LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
tracing::debug!("Formatting document {}", file_path.display());
let open_files = self.open_files.read().await;
if let Some(content) = open_files.get(file_path) {
Ok(content.clone())
} else {
Err(LspError::FileNotOpen(file_path.to_path_buf()))
}
}
pub async fn format_range(&self, file_path: &Path, range: Range) -> Result<String, LspError> {
if !self.is_initialized().await {
return Err(LspError::NotInitialized);
}
tracing::debug!("Formatting range in {} at {}:{} to {}:{}",
file_path.display(), range.start.line, range.start.character,
range.end.line, range.end.character);
let open_files = self.open_files.read().await;
if let Some(content) = open_files.get(file_path) {
Ok(content.clone())
} else {
Err(LspError::FileNotOpen(file_path.to_path_buf()))
}
}
pub async fn _simulate_diagnostics(&self, file_path: &Path, diagnostics: Vec<LspDiagnostic>) {
let mut diag_map = self.diagnostics.write().await;
diag_map.insert(file_path.to_path_buf(), diagnostics);
}
}
#[async_trait]
impl LspService for LspClient {
async fn get_diagnostics(&self, file_path: &Path) -> Result<Vec<LspDiagnostic>, LspError> {
self.get_diagnostics(file_path).await
}
async fn did_change_file(&self, file_path: &Path, content: &str) -> Result<(), LspError> {
self.did_change(file_path, content).await
}
async fn did_open_file(&self, file_path: &Path, content: &str) -> Result<(), LspError> {
self.did_open(file_path, content).await
}
async fn did_close_file(&self, file_path: &Path) -> Result<(), LspError> {
self.did_close(file_path).await
}
fn supports_file(&self, file_path: &Path) -> bool {
self.handles_file(file_path)
}
async fn get_completions(&self, file_path: &Path, position: Position) -> Result<Vec<CompletionItem>, LspError> {
self.get_completions(file_path, position).await
}
async fn goto_definition(&self, file_path: &Path, position: Position) -> Result<Vec<Location>, LspError> {
self.goto_definition(file_path, position).await
}
async fn get_hover(&self, file_path: &Path, position: Position) -> Result<Option<Hover>, LspError> {
self.get_hover(file_path, position).await
}
async fn find_references(&self, file_path: &Path, position: Position, include_declaration: bool) -> Result<Vec<Location>, LspError> {
self.find_references(file_path, position, include_declaration).await
}
async fn get_code_actions(&self, file_path: &Path, range: Range) -> Result<Vec<CodeAction>, LspError> {
self.get_code_actions(file_path, range).await
}
async fn get_document_symbols(&self, file_path: &Path) -> Result<Vec<SymbolInformation>, LspError> {
self.get_document_symbols(file_path).await
}
async fn format_document(&self, file_path: &Path) -> Result<String, LspError> {
self.format_document(file_path).await
}
async fn format_range(&self, file_path: &Path, range: Range) -> Result<String, LspError> {
self.format_range(file_path, range).await
}
}
impl Drop for LspClient {
fn drop(&mut self) {
if let Some(mut process) = self.process.take() {
tokio::spawn(async move {
if let Err(e) = process.kill().await {
tracing::warn!("Failed to kill LSP process in drop: {}", e);
}
});
}
}
}
fn path_to_uri(path: &Path) -> Result<Url, LspError> {
Url::from_file_path(path)
.map_err(|_| LspError::InvalidResponse(format!("Invalid file path: {}", path.display())))
}
fn uri_to_path(uri: &Url) -> Result<PathBuf, LspError> {
uri.to_file_path()
.map_err(|_| LspError::InvalidResponse(format!("Invalid URI: {}", uri)))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_client_creation() {
let config = LspServerConfig::default();
let client = LspClient::new(config);
assert!(!client.is_initialized().await);
assert!(client.get_diagnostics(&PathBuf::from("test.rs")).await.unwrap().is_empty());
}
#[tokio::test]
async fn test_file_operations() {
let config = LspServerConfig::default();
let mut client = LspClient::new(config);
{
let mut initialized = client.initialized.write().await;
*initialized = true;
}
let file_path = PathBuf::from("test.rs");
let content = "fn main() {}";
assert!(client.did_open(&file_path, content).await.is_ok());
assert!(client.did_change(&file_path, "fn main() { println!(\"Hello\"); }").await.is_ok());
assert!(client.did_close(&file_path).await.is_ok());
}
#[test]
fn test_path_uri_conversion() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let path = temp_dir.path().join("test.rs");
let uri = path_to_uri(&path).unwrap();
let converted_path = uri_to_path(&uri).unwrap();
assert_eq!(path, converted_path);
}
}