use crate::application::services::BookmarkService;
use crate::domain::bookmark::Bookmark;
use crate::domain::error::{DomainError, DomainResult};
use crate::domain::repositories::query::{BookmarkQuery, SortCriteria, SortDirection, SortField};
use crate::domain::tag::Tag;
use crate::lsp::domain::LanguageRegistry;
use crate::lsp::error::{LspError, LspResult};
use serde_json::{json, Value};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tower_lsp::lsp_types::{Position, Range, TextEdit, Url, WorkspaceEdit};
use tracing::{debug, instrument};
#[derive(Debug)]
pub struct CommandService {
bookmark_service: Arc<dyn BookmarkService>,
}
impl CommandService {
pub fn with_service(bookmark_service: Arc<dyn BookmarkService>) -> Self {
Self { bookmark_service }
}
#[instrument(skip(self))]
pub fn create_snippet(
&self,
url: &str,
title: &str,
description: Option<&str>,
tags: Vec<String>,
) -> LspResult<Value> {
debug!("Creating snippet: title={}, tags={:?}", title, tags);
let mut tag_set = HashSet::new();
tag_set.insert(
Tag::new("_snip_")
.map_err(LspError::from)
.map_err(|e| e.context("creating _snip_ system tag for snippet creation"))?,
);
for tag in tags {
tag_set.insert(
Tag::new(&tag)
.map_err(LspError::from)
.map_err(|e| e.context(format!("creating user tag: {}", tag)))?,
);
}
let bookmark = self
.bookmark_service
.add_bookmark(
url,
Some(title),
description,
Some(&tag_set),
false, true, )
.map_err(LspError::from)
.map_err(|e| e.context("adding snippet bookmark via service"))?;
Ok(Self::bookmark_to_snippet_json(&bookmark))
}
#[instrument(skip(self))]
pub fn list_snippets(&self, language_id: Option<&str>) -> LspResult<Value> {
debug!("Listing snippets for language: {:?}", language_id);
let mut query = BookmarkQuery::default();
let snip_tag = Tag::new("_snip_")
.map_err(LspError::from)
.map_err(|e| e.context("creating _snip_ tag for snippet listing"))?;
let mut tags_all = HashSet::new();
tags_all.insert(snip_tag);
if let Some(lang) = language_id {
let language_tag = Self::map_language_id_to_tag(lang);
let lang_tag = Tag::new(&language_tag).map_err(LspError::from)?;
tags_all.insert(lang_tag);
}
query.tags_all = Some(tags_all);
query.sort = Some(SortCriteria::new(SortField::Modified, SortDirection::Descending));
let bookmarks = self
.bookmark_service
.search_bookmarks(&query)
.map_err(LspError::from)?;
let snippets: Vec<Value> = bookmarks
.iter()
.map(Self::bookmark_to_snippet_json)
.collect();
Ok(json!({
"snippets": snippets
}))
}
#[instrument(skip(self))]
pub fn get_snippet(&self, id: i32) -> LspResult<Value> {
debug!("Getting snippet with ID: {}", id);
let bookmark = self
.bookmark_service
.get_bookmark(id)
.map_err(LspError::from)?
.ok_or_else(|| LspError::NotFound(format!("Snippet with ID {} not found", id)))?;
if !bookmark.tags.iter().any(|t| t.value() == "_snip_") {
return Err(LspError::InvalidInput(format!(
"Bookmark {} is not a snippet",
id
)));
}
Ok(Self::bookmark_to_snippet_json(&bookmark))
}
#[instrument(skip(self))]
pub fn update_snippet(
&self,
id: i32,
url: Option<&str>,
title: Option<&str>,
description: Option<&str>,
tags: Option<Vec<String>>,
) -> LspResult<Value> {
debug!(
"Updating snippet {}: title={:?}, tags={:?}",
id, title, tags
);
let mut bookmark = self
.bookmark_service
.get_bookmark(id)
.map_err(LspError::from)?
.ok_or_else(|| LspError::NotFound(format!("Snippet with ID {} not found", id)))?;
if !bookmark.tags.iter().any(|t| t.value() == "_snip_") {
return Err(LspError::InvalidInput(format!(
"Bookmark {} is not a snippet",
id
)));
}
if let Some(new_url) = url {
bookmark.url = new_url.to_string();
}
if let Some(new_title) = title {
bookmark.title = new_title.to_string();
}
if let Some(new_desc) = description {
bookmark.description = new_desc.to_string();
}
let mut updated = self
.bookmark_service
.update_bookmark(bookmark, false)
.map_err(LspError::from)?;
if let Some(new_tags) = tags {
let mut tag_set = HashSet::new();
tag_set.insert(Tag::new("_snip_").map_err(LspError::from)?);
for tag in new_tags {
tag_set.insert(Tag::new(&tag).map_err(LspError::from)?);
}
updated = self
.bookmark_service
.replace_bookmark_tags(id, &tag_set)
.map_err(LspError::from)?;
}
Ok(Self::bookmark_to_snippet_json(&updated))
}
#[instrument(skip(self))]
pub fn delete_snippet(&self, id: i32) -> LspResult<Value> {
debug!("Deleting snippet with ID: {}", id);
let bookmark = self
.bookmark_service
.get_bookmark(id)
.map_err(LspError::from)?
.ok_or_else(|| LspError::NotFound(format!("Snippet with ID {} not found", id)))?;
if !bookmark.tags.iter().any(|t| t.value() == "_snip_") {
return Err(LspError::InvalidInput(format!(
"Bookmark {} is not a snippet",
id
)));
}
let deleted = self
.bookmark_service
.delete_bookmark(id)
.map_err(LspError::from)?;
Ok(json!({
"success": deleted,
"id": id
}))
}
fn bookmark_to_snippet_json(bookmark: &Bookmark) -> Value {
json!({
"id": bookmark.id,
"url": bookmark.url,
"title": bookmark.title,
"description": bookmark.description,
"tags": bookmark.tags.iter().map(|t| t.value()).collect::<Vec<_>>(),
})
}
fn map_language_id_to_tag(language_id: &str) -> String {
match language_id {
"rust" => "rust",
"python" => "python",
"javascript" | "javascriptreact" => "js",
"typescript" | "typescriptreact" => "ts",
"shellscript" | "bash" | "sh" => "sh",
"go" => "go",
"java" => "java",
"cpp" | "c" => "cpp",
"html" => "html",
"css" | "scss" | "sass" => "css",
"markdown" => "md",
"yaml" => "yaml",
"json" => "json",
"sql" => "sql",
"ruby" => "ruby",
"php" => "php",
_ => language_id,
}
.to_string()
}
#[instrument(skip(file_uri))]
pub fn insert_filepath_comment(file_uri: &str) -> DomainResult<WorkspaceEdit> {
let relative_path = Self::get_relative_path(file_uri)?;
let comment_syntax = LanguageRegistry::get_comment_syntax(file_uri);
let comment_text = match comment_syntax {
"<!--" => format!("<!-- {} -->\n", relative_path),
"/*" => format!("/* {} */\n", relative_path),
_ => format!("{} {}\n", comment_syntax, relative_path),
};
debug!("Inserting filepath comment: {}", comment_text.trim());
let edit = TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
},
new_text: comment_text,
};
let uri = Url::parse(file_uri)
.map_err(|e| DomainError::Other(format!("Parse file URI for workspace edit: {}", e)))?;
let mut changes = HashMap::new();
changes.insert(uri, vec![edit]);
Ok(WorkspaceEdit {
changes: Some(changes),
document_changes: None,
change_annotations: None,
})
}
fn get_relative_path(file_uri: &str) -> DomainResult<String> {
let url = Url::parse(file_uri)
.map_err(|e| DomainError::Other(format!("Parse file URI: {}", e)))?;
let file_path = url
.to_file_path()
.map_err(|_| DomainError::Other("Convert URL to file path".to_string()))?;
let mut current = file_path.as_path();
while let Some(parent) = current.parent() {
if parent.join("Cargo.toml").exists()
|| parent.join("package.json").exists()
|| parent.join("pom.xml").exists()
|| parent.join("build.gradle").exists()
|| parent.join("build.gradle.kts").exists()
|| parent.join("Makefile").exists()
|| parent.join(".git").exists()
{
if let Ok(rel_path) = file_path.strip_prefix(parent) {
return Ok(rel_path.to_string_lossy().to_string());
}
break;
}
current = parent;
}
file_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.ok_or_else(|| DomainError::Other("Extract filename from file path".to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::application::services::bookmark_service_impl::BookmarkServiceImpl;
use crate::infrastructure::repositories::json_import_repository::JsonImportRepository;
use crate::infrastructure::repositories::null_vector_repository::NullVectorRepository;
use crate::util::testing::{init_test_env, setup_test_db, EnvGuard};
use std::sync::Arc;
#[test]
fn given_valid_snippet_data_when_creating_then_returns_snippet_with_id() {
let _env = init_test_env();
let _guard = EnvGuard::new();
let repository = setup_test_db();
let repository_arc = Arc::new(repository);
let embedder = Arc::new(crate::infrastructure::embeddings::DummyEmbedding);
let bookmark_service = Arc::new(BookmarkServiceImpl::new(
repository_arc,
embedder,
Arc::new(NullVectorRepository),
Arc::new(JsonImportRepository::new()),
));
let service = CommandService::with_service(bookmark_service);
let result = service.create_snippet(
"fn example_test_snippet() { println!(\"Hello from test\"); }",
"Example Test Function",
Some("A simple example function"),
vec!["rust".to_string(), "example".to_string()],
);
assert!(result.is_ok());
let json = result.unwrap();
assert!(json.get("id").is_some());
assert_eq!(
json.get("title").unwrap().as_str().unwrap(),
"Example Test Function"
);
assert_eq!(
json.get("url").unwrap().as_str().unwrap(),
"fn example_test_snippet() { println!(\"Hello from test\"); }"
);
let tags = json.get("tags").unwrap().as_array().unwrap();
assert!(tags.iter().any(|t| t.as_str() == Some("_snip_")));
assert!(tags.iter().any(|t| t.as_str() == Some("rust")));
}
#[test]
fn given_language_filter_when_listing_snippets_then_returns_filtered_results() {
let _env = init_test_env();
let _guard = EnvGuard::new();
let repository = setup_test_db();
let repository_arc = Arc::new(repository);
let embedder = Arc::new(crate::infrastructure::embeddings::DummyEmbedding);
let bookmark_service = Arc::new(BookmarkServiceImpl::new(
repository_arc,
embedder,
Arc::new(NullVectorRepository),
Arc::new(JsonImportRepository::new()),
));
let service = CommandService::with_service(bookmark_service);
service
.create_snippet(
"print('Python snippet test')",
"Python Print Test",
None,
vec!["python".to_string()],
)
.unwrap();
service
.create_snippet(
"fn rust_test() { println!(\"test\"); }",
"Rust Function Test",
None,
vec!["rust".to_string()],
)
.unwrap();
let result = service.list_snippets(Some("rust"));
assert!(result.is_ok());
let json = result.unwrap();
let snippets = json.get("snippets").unwrap().as_array().unwrap();
for snippet in snippets {
let tags = snippet.get("tags").unwrap().as_array().unwrap();
assert!(tags.iter().any(|t| t.as_str() == Some("rust")));
}
}
#[test]
fn given_existing_snippet_when_updating_then_preserves_system_tag() {
let _env = init_test_env();
let _guard = EnvGuard::new();
let repository = setup_test_db();
let repository_arc = Arc::new(repository);
let embedder = Arc::new(crate::infrastructure::embeddings::DummyEmbedding);
let bookmark_service = Arc::new(BookmarkServiceImpl::new(
repository_arc,
embedder,
Arc::new(NullVectorRepository),
Arc::new(JsonImportRepository::new()),
));
let service = CommandService::with_service(bookmark_service);
let created = service
.create_snippet(
"original test content for update",
"Original Test Title",
None,
vec!["rust".to_string()],
)
.unwrap();
let id = created.get("id").unwrap().as_i64().unwrap() as i32;
let result = service.update_snippet(
id,
Some("updated test content for test"),
Some("Updated Title"),
Some("Updated description"),
Some(vec!["python".to_string(), "updated".to_string()]),
);
assert!(result.is_ok());
let updated = result.unwrap();
assert_eq!(
updated.get("url").unwrap().as_str().unwrap(),
"updated test content for test"
);
assert_eq!(
updated.get("title").unwrap().as_str().unwrap(),
"Updated Title"
);
let tags = updated.get("tags").unwrap().as_array().unwrap();
assert!(tags.iter().any(|t| t.as_str() == Some("_snip_"))); assert!(tags.iter().any(|t| t.as_str() == Some("python")));
assert!(tags.iter().any(|t| t.as_str() == Some("updated")));
assert!(!tags.iter().any(|t| t.as_str() == Some("rust"))); }
#[test]
fn given_non_snippet_bookmark_when_getting_as_snippet_then_returns_error() {
let _env = init_test_env();
let _guard = EnvGuard::new();
let repository = setup_test_db();
let repository_arc = Arc::new(repository);
let embedder = Arc::new(crate::infrastructure::embeddings::DummyEmbedding);
let bookmark_service = Arc::new(BookmarkServiceImpl::new(
repository_arc,
embedder,
Arc::new(NullVectorRepository),
Arc::new(JsonImportRepository::new()),
));
let service = CommandService::with_service(bookmark_service.clone());
use std::collections::HashSet;
let mut tags = HashSet::new();
tags.insert(Tag::new("website").unwrap());
let bookmark = bookmark_service
.add_bookmark(
"https://example-test-non-snippet.com",
Some("Example Test Site"),
None,
Some(&tags),
false,
true,
)
.unwrap();
let id = bookmark.id.unwrap();
let result = service.get_snippet(id);
assert!(result.is_err());
match result.unwrap_err() {
LspError::InvalidInput(msg) => assert!(msg.contains("not a snippet")),
_ => panic!("Expected InvalidInput error"),
}
}
#[test]
fn given_non_existent_id_when_deleting_snippet_then_returns_not_found() {
let _env = init_test_env();
let _guard = EnvGuard::new();
let repository = setup_test_db();
let repository_arc = Arc::new(repository);
let embedder = Arc::new(crate::infrastructure::embeddings::DummyEmbedding);
let bookmark_service = Arc::new(BookmarkServiceImpl::new(
repository_arc,
embedder,
Arc::new(NullVectorRepository),
Arc::new(JsonImportRepository::new()),
));
let service = CommandService::with_service(bookmark_service);
let result = service.delete_snippet(99999);
assert!(result.is_err());
match result.unwrap_err() {
LspError::NotFound(msg) => assert!(msg.contains("99999")),
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn given_rust_file_when_inserting_filepath_comment_then_uses_double_slash() {
let file_uri = "file:///path/to/test.rs";
let result = CommandService::insert_filepath_comment(file_uri);
assert!(result.is_ok());
let workspace_edit = result.expect("valid workspace edit");
let changes = workspace_edit.changes.expect("workspace changes");
let edits = changes.values().next().expect("text edits");
let edit = &edits[0];
assert!(edit.new_text.starts_with("// "));
assert!(edit.new_text.contains("test.rs"));
}
#[test]
fn given_html_file_when_inserting_filepath_comment_then_uses_html_comment() {
let file_uri = "file:///path/to/test.html";
let result = CommandService::insert_filepath_comment(file_uri);
assert!(result.is_ok());
let workspace_edit = result.expect("valid workspace edit");
let changes = workspace_edit.changes.expect("workspace changes");
let edits = changes.values().next().expect("text edits");
let edit = &edits[0];
assert!(edit.new_text.starts_with("<!-- "));
assert!(edit.new_text.ends_with(" -->\n"));
assert!(edit.new_text.contains("test.html"));
}
#[test]
fn given_python_file_when_inserting_filepath_comment_then_uses_hash() {
let file_uri = "file:///path/to/test.py";
let result = CommandService::insert_filepath_comment(file_uri);
assert!(result.is_ok());
let workspace_edit = result.expect("valid workspace edit");
let changes = workspace_edit.changes.expect("workspace changes");
let edits = changes.values().next().expect("text edits");
let edit = &edits[0];
assert!(edit.new_text.starts_with("# "));
assert!(edit.new_text.contains("test.py"));
}
#[test]
fn given_invalid_uri_when_inserting_filepath_comment_then_returns_error() {
let file_uri = "invalid-uri";
let result = CommandService::insert_filepath_comment(file_uri);
assert!(result.is_err());
}
#[test]
fn given_file_when_getting_relative_path_then_returns_filename_fallback() {
let file_uri = "file:///some/deep/path/test.rs";
let result = CommandService::get_relative_path(file_uri);
assert!(result.is_ok());
let path = result.expect("valid relative path");
assert_eq!(path, "test.rs"); }
}