mcp-cpp-server 0.2.2

A high-performance Model Context Protocol (MCP) server for C++ code analysis using clangd LSP integration
//! Index triggering abstraction
//!
//! This module provides the IndexTrigger trait for decoupling index triggering
//! from the component monitor. This allows ComponentIndexMonitor to trigger
//! indexing without knowing about ClangdSession details.

use async_trait::async_trait;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::debug;

use crate::clangd::file_manager::ClangdFileManager;
use crate::clangd::session::{ClangdSession, ClangdSessionTrait};
use crate::lsp::traits::LspClientTrait;
use crate::project::ProjectError;

/// Trait for triggering indexing operations
///
/// This trait encapsulates the mechanism for initiating indexing of specific files,
/// allowing ComponentIndexMonitor to trigger indexing without depending on
/// ClangdSession directly.
#[cfg_attr(test, mockall::automock)]
#[async_trait]
pub trait IndexTrigger: Send + Sync {
    /// Trigger indexing by opening the specified file
    ///
    /// This method should cause the underlying indexing system (e.g., clangd)
    /// to begin indexing the specified file and its dependencies.
    ///
    /// # Arguments
    /// * `file_path` - Path to the source file to trigger indexing for
    ///
    /// # Returns
    /// * `Ok(())` if indexing was successfully triggered
    /// * `Err(ProjectError)` if triggering failed
    async fn trigger(&self, file_path: &Path) -> Result<(), ProjectError>;
}

/// Implementation of IndexTrigger that uses ClangdSession and FileManager
///
/// This implementation encapsulates the ClangdSession and FileManager dependencies
/// and provides the bridge between ComponentIndexMonitor and clangd for triggering indexing.
/// Uses proper file management through the injected FileManager.
pub struct ClangdIndexTrigger {
    /// The clangd session to use for triggering indexing
    session: Arc<Mutex<ClangdSession>>,
    /// File manager for proper file state management
    file_manager: Arc<Mutex<ClangdFileManager>>,
}

impl ClangdIndexTrigger {
    /// Create a new ClangdIndexTrigger with the given session and file manager
    ///
    /// # Arguments
    /// * `session` - The ClangdSession to use for triggering indexing
    /// * `file_manager` - The FileManager for proper file state management
    pub fn new(
        session: Arc<Mutex<ClangdSession>>,
        file_manager: Arc<Mutex<ClangdFileManager>>,
    ) -> Self {
        debug!("Created ClangdIndexTrigger");
        Self {
            session,
            file_manager,
        }
    }
}

#[async_trait]
impl IndexTrigger for ClangdIndexTrigger {
    async fn trigger(&self, file_path: &Path) -> Result<(), ProjectError> {
        debug!("Triggering indexing for file: {:?}", file_path);

        // Ensure file is ready using proper file management
        let mut session = self.session.lock().await;
        let mut file_manager = self.file_manager.lock().await;

        file_manager
            .ensure_file_ready(file_path, session.client_mut())
            .await
            .map_err(|e| {
                ProjectError::IndexingTrigger(format!(
                    "Failed to ensure file ready for indexing {}: {}",
                    file_path.display(),
                    e
                ))
            })?;

        // Request document symbols to force additional symbol processing
        let file_uri = crate::symbol::uri_from_pathbuf(file_path);
        debug!("Requesting document symbols for file: {:?}", file_path);

        let client = session.client_mut();
        match client.text_document_document_symbol(file_uri).await {
            Ok(_) => {
                debug!(
                    "Successfully retrieved document symbols for file: {:?}",
                    file_path
                );
            }
            Err(e) => {
                debug!(
                    "Failed to retrieve document symbols for {}: {} (continuing anyway)",
                    file_path.display(),
                    e
                );
                // Don't fail the trigger operation if document symbols fails
            }
        }

        debug!("Successfully triggered indexing for file: {:?}", file_path);
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[tokio::test]
    async fn test_mock_index_trigger() {
        let mut mock_trigger = MockIndexTrigger::new();

        let test_path = PathBuf::from("/test/file.cpp");
        let expected_path = test_path.clone();
        mock_trigger
            .expect_trigger()
            .with(mockall::predicate::function(move |path: &Path| {
                path == expected_path
            }))
            .times(1)
            .returning(|_| Ok(()));

        let result = mock_trigger.trigger(&test_path).await;
        assert!(result.is_ok());
    }

    #[tokio::test]
    async fn test_mock_index_trigger_failure() {
        let mut mock_trigger = MockIndexTrigger::new();

        let test_path = PathBuf::from("/test/file.cpp");
        let expected_path = test_path.clone();
        mock_trigger
            .expect_trigger()
            .with(mockall::predicate::function(move |path: &Path| {
                path == expected_path
            }))
            .times(1)
            .returning(|_| Err(ProjectError::IndexingTrigger("Test error".to_string())));

        let result = mock_trigger.trigger(&test_path).await;
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("Test error"));
    }
}