use lex_babel::{FormatRegistry, SerializedDocument};
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tower_lsp::jsonrpc::{Error, Result};
use tower_lsp::lsp_types::Url;
pub const COMMAND_ECHO: &str = "lex.echo";
pub const COMMAND_IMPORT: &str = "lex.import";
pub const COMMAND_EXPORT: &str = "lex.export";
pub const COMMAND_NEXT_ANNOTATION: &str = "lex.next_annotation";
pub const COMMAND_PREVIOUS_ANNOTATION: &str = "lex.previous_annotation";
pub const COMMAND_RESOLVE_ANNOTATION: &str = "lex.resolve_annotation";
pub const COMMAND_TOGGLE_ANNOTATIONS: &str = "lex.toggle_annotations";
pub const COMMAND_INSERT_ASSET: &str = "lex.insert_asset";
pub const COMMAND_INSERT_VERBATIM: &str = "lex.insert_verbatim";
pub const COMMAND_FOOTNOTES_REORDER: &str = "lex.footnotes.reorder";
pub const COMMAND_TABLE_FORMAT: &str = "lex.table.format";
pub fn execute_command(command: &str, arguments: &[Value]) -> Result<Option<Value>> {
match command {
COMMAND_ECHO => {
let msg = arguments
.first()
.and_then(|v| v.as_str())
.unwrap_or("default echo");
Ok(Some(Value::String(format!("Echo: {msg}"))))
}
COMMAND_IMPORT => {
let format = arguments
.first()
.and_then(|v| v.as_str())
.ok_or_else(|| Error::invalid_params("Missing 'format' argument"))?;
let content = arguments
.get(1)
.and_then(|v| v.as_str())
.ok_or_else(|| Error::invalid_params("Missing 'content' argument"))?;
let registry = FormatRegistry::with_defaults();
let doc = registry
.parse(content, format)
.map_err(|e| Error::invalid_params(format!("Import failed: {e}")))?;
let lex_content = registry
.serialize(&doc, "lex")
.map_err(|_e| Error::internal_error())?;
Ok(Some(Value::String(lex_content)))
}
COMMAND_EXPORT => {
let format = arguments
.first()
.and_then(|v| v.as_str())
.ok_or_else(|| Error::invalid_params("Missing 'format' argument"))?;
let content = arguments
.get(1)
.and_then(|v| v.as_str())
.ok_or_else(|| Error::invalid_params("Missing 'content' argument"))?;
let source_uri = arguments.get(2).and_then(|v| v.as_str());
let output_path = arguments.get(3).and_then(|v| v.as_str());
let registry = FormatRegistry::with_defaults();
let doc = registry.parse(content, "lex").map_err(|e| {
Error::invalid_params(format!("Export failed to parse source: {e}"))
})?;
let serialized = registry
.serialize_with_options(&doc, format, &HashMap::new())
.map_err(|e| Error::invalid_params(format!("Export failed: {e}")))?;
match serialized {
SerializedDocument::Text(text) => Ok(Some(Value::String(text))),
SerializedDocument::Binary(bytes) => {
let path = if let Some(path) = output_path {
PathBuf::from(path)
} else if let Some(uri) = source_uri {
let url = Url::parse(uri)
.map_err(|_| Error::invalid_params("Invalid source URI"))?;
let mut path = url
.to_file_path()
.map_err(|_| Error::invalid_params("Source URI is not a file"))?;
path.set_extension(format);
path
} else {
return Err(Error::invalid_params(
"Binary export requires 'outputPath' or 'sourceUri'",
));
};
fs::write(&path, bytes).map_err(|_e| Error::internal_error())?;
Ok(Some(Value::String(path.to_string_lossy().to_string())))
}
}
}
COMMAND_FOOTNOTES_REORDER => {
let content = arguments
.first()
.and_then(|v| v.as_str())
.ok_or_else(|| Error::invalid_params("Missing 'content' argument"))?;
let doc = lex_core::lex::parsing::parse_document(content)
.map_err(|_| Error::invalid_params("Failed to parse document"))?;
let new_content = crate::features::footnotes::reorder_footnotes(&doc, content);
Ok(Some(Value::String(new_content)))
}
COMMAND_TABLE_FORMAT => {
let content = arguments
.first()
.and_then(|v| v.as_str())
.ok_or_else(|| Error::invalid_params("Missing 'content' argument"))?;
let line = arguments
.get(1)
.and_then(|v| v.as_u64())
.ok_or_else(|| Error::invalid_params("Missing 'line' argument"))?
as usize;
let column = arguments
.get(2)
.and_then(|v| v.as_u64())
.ok_or_else(|| Error::invalid_params("Missing 'column' argument"))?
as usize;
let doc = lex_core::lex::parsing::parse_document(content)
.map_err(|_| Error::invalid_params("Failed to parse document"))?;
let position = lex_core::lex::ast::Position::new(line, column);
let edit = crate::features::table_format::format_table_at(&doc, content, position);
match edit {
Some(edit) => Ok(Some(serde_json::json!({
"start": edit.start,
"end": edit.end,
"newText": edit.new_text,
}))),
None => Ok(None),
}
}
_ => Err(Error::invalid_request()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_import_markdown() {
let content = "# Hello\n\nWorld";
let args = vec![json!("markdown"), json!(content)];
let result = execute_command(COMMAND_IMPORT, &args).unwrap();
let lex_string = result.unwrap().as_str().unwrap().to_string();
assert!(lex_string.contains("Hello"));
assert!(lex_string.contains("World"));
}
#[test]
fn test_import_missing_args() {
let args = vec![];
let result = execute_command(COMMAND_IMPORT, &args);
assert!(result.is_err());
}
#[test]
fn test_import_invalid_format() {
let args = vec![json!("invalid_format"), json!("content")];
let result = execute_command(COMMAND_IMPORT, &args);
assert!(result.is_err());
}
#[test]
fn test_export_markdown() {
let content = "Hello World"; let args = vec![json!("markdown"), json!(content)];
let result = execute_command(COMMAND_EXPORT, &args).unwrap();
let md_string = result.unwrap().as_str().unwrap().to_string();
assert!(md_string.contains("Hello World"));
}
#[test]
fn test_export_binary_with_output_path() {
let content = "Hello";
let args = vec![
json!("markdown"),
json!(content),
json!("file:///tmp/source.lex"),
];
let result = execute_command(COMMAND_EXPORT, &args).unwrap();
let md_string = result.unwrap().as_str().unwrap().to_string();
assert!(md_string.contains("Hello"));
}
}