use crate::application::services::bookmark_service::BookmarkService;
use crate::application::services::InterpolationService;
use crate::domain::repositories::query::BookmarkQuery;
use crate::lsp::domain::{Snippet, SnippetFilter};
use crate::util::interpolation::InterpolationHelper;
use async_trait::async_trait;
use std::sync::Arc;
use tracing::{debug, instrument};
pub type SnippetResult<T> = Result<T, SnippetError>;
#[derive(Debug, thiserror::Error)]
pub enum SnippetError {
#[error("Application error: {0}")]
Application(#[from] crate::application::error::ApplicationError),
#[error("Service error: {0}")]
Service(String),
}
#[derive(Debug)]
pub struct LspSnippetService {
bookmark_service: Arc<dyn BookmarkService>,
interpolation_service: Arc<dyn InterpolationService>,
}
impl LspSnippetService {
pub fn with_services(
bookmark_service: Arc<dyn BookmarkService>,
interpolation_service: Arc<dyn InterpolationService>,
) -> Self {
Self {
bookmark_service,
interpolation_service,
}
}
pub fn with_service(bookmark_service: Arc<dyn BookmarkService>) -> Self {
use crate::application::InterpolationServiceImpl;
use crate::infrastructure::interpolation::minijinja_engine::{
MiniJinjaEngine, SafeShellExecutor,
};
use std::sync::Arc;
let shell_executor = Arc::new(SafeShellExecutor::new());
let template_engine = Arc::new(MiniJinjaEngine::new(shell_executor));
let interpolation_service = Arc::new(InterpolationServiceImpl::new(template_engine));
Self {
bookmark_service,
interpolation_service,
}
}
}
#[async_trait]
pub trait AsyncSnippetService: Send + Sync {
async fn fetch_snippets(&self, filter: &SnippetFilter) -> SnippetResult<Vec<Snippet>>;
async fn health_check(&self) -> SnippetResult<()>;
}
#[async_trait]
impl AsyncSnippetService for LspSnippetService {
#[instrument(skip(self))]
async fn fetch_snippets(&self, filter: &SnippetFilter) -> SnippetResult<Vec<Snippet>> {
debug!("Fetching snippets with filter: {:?}", filter);
let bookmark_service = Arc::clone(&self.bookmark_service);
let filter_clone = filter.clone();
let bookmarks = tokio::task::spawn_blocking(move || {
let mut query = BookmarkQuery::new();
let mut text_parts = Vec::new();
if let Some(fts_query) = filter_clone.build_fts_query() {
text_parts.push(fts_query);
}
if let Some(ref prefix) = filter_clone.query_prefix {
if !prefix.trim().is_empty() {
text_parts.push(format!("metadata:{}*", prefix));
}
}
if !text_parts.is_empty() {
let combined_query = if text_parts.len() == 1 {
text_parts.into_iter().next().unwrap()
} else {
text_parts.join(" AND ")
};
query = query.with_text_query(Some(&combined_query));
}
if filter_clone.max_results > 0 {
query = query.with_limit(Some(filter_clone.max_results));
}
debug!("Executing bookmark search with query: {:?}", query);
bookmark_service.search_bookmarks(&query)
})
.await
.map_err(|e| SnippetError::Service(format!("Task join error: {}", e)))?
.map_err(SnippetError::Application)?;
debug!("Found {} bookmarks", bookmarks.len());
let interpolation_service = Arc::clone(&self.interpolation_service);
let enable_interpolation = filter.enable_interpolation;
let snippets: Vec<Snippet> = bookmarks
.into_iter()
.map(|bookmark| {
let content = if enable_interpolation {
match InterpolationHelper::render_if_needed(
&bookmark.url, &bookmark,
&interpolation_service,
"lsp snippet",
) {
Ok(interpolated) => {
debug!(
"Template interpolation successful for bookmark: {}",
bookmark.title
);
interpolated
}
Err(e) => {
debug!(
"Template interpolation failed for bookmark {}: {}, using raw content",
bookmark.title, e
);
bookmark.url.clone()
}
}
} else {
debug!(
"Template interpolation disabled for bookmark: {}",
bookmark.title
);
bookmark.url.clone()
};
Snippet {
id: bookmark.id.unwrap_or(0), title: bookmark.title, content, description: bookmark.description,
tags: bookmark
.tags
.into_iter()
.map(|tag| tag.value().to_string())
.collect(), access_count: bookmark.access_count,
}
})
.collect();
debug!("Converted to {} snippets", snippets.len());
Ok(snippets)
}
#[instrument(skip(self))]
async fn health_check(&self) -> SnippetResult<()> {
debug!("Performing health check");
let bookmark_service = Arc::clone(&self.bookmark_service);
tokio::task::spawn_blocking(move || {
let query = BookmarkQuery::new().with_limit(Some(1));
bookmark_service.search_bookmarks(&query)
})
.await
.map_err(|e| SnippetError::Service(format!("Health check task failed: {}", e)))?
.map_err(SnippetError::Application)?;
debug!("Health check passed");
Ok(())
}
}