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 const COMMAND_TABLE_NEXT_CELL: &str = "lex.table.next_cell";
pub const COMMAND_TABLE_PREVIOUS_CELL: &str = "lex.table.previous_cell";
pub const COMMAND_FORMATS_LIST: &str = "lex.formats.list";
pub const COMMAND_EXTRACT_TO_INCLUDE: &str = "lex.extractToInclude";
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),
}
}
COMMAND_TABLE_NEXT_CELL => {
let outcome = navigate_table_cell_command(
arguments,
crate::features::table_navigation::Direction::Next,
)?;
Ok(Some(
serde_json::to_value(outcome).map_err(|_| Error::internal_error())?,
))
}
COMMAND_TABLE_PREVIOUS_CELL => {
let outcome = navigate_table_cell_command(
arguments,
crate::features::table_navigation::Direction::Previous,
)?;
Ok(Some(
serde_json::to_value(outcome).map_err(|_| Error::internal_error())?,
))
}
COMMAND_FORMATS_LIST => {
let registry = FormatRegistry::with_defaults();
let descriptors = list_formats(®istry)?;
Ok(Some(
serde_json::to_value(descriptors).map_err(|_| Error::internal_error())?,
))
}
_ => Err(Error::invalid_request()),
}
}
fn navigate_table_cell_command(
arguments: &[Value],
direction: crate::features::table_navigation::Direction,
) -> Result<crate::features::table_navigation::TableNavOutcome> {
let content = arguments
.first()
.and_then(|v| v.as_str())
.ok_or_else(|| Error::invalid_params("Missing 'content' argument"))?;
let line = arg_as_usize(arguments.get(1), "line")?;
let column = arg_as_usize(arguments.get(2), "column")?;
Ok(crate::features::table_navigation::navigate_table_cell(
content, line, column, direction,
))
}
fn arg_as_usize(value: Option<&Value>, name: &str) -> Result<usize> {
let raw = value
.and_then(|v| v.as_u64())
.ok_or_else(|| Error::invalid_params(format!("Missing '{name}' argument")))?;
usize::try_from(raw)
.map_err(|_| Error::invalid_params(format!("'{name}' argument does not fit in usize")))
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct FormatDescriptor {
name: String,
description: String,
supports_parsing: bool,
supports_serialization: bool,
file_extensions: Vec<String>,
}
fn list_formats(registry: &FormatRegistry) -> Result<Vec<FormatDescriptor>> {
registry
.list_formats()
.into_iter()
.map(|name| {
let format = registry.get(&name).map_err(|_| Error::internal_error())?;
Ok(FormatDescriptor {
name: format.name().to_string(),
description: format.description().to_string(),
supports_parsing: format.supports_parsing(),
supports_serialization: format.supports_serialization(),
file_extensions: format
.file_extensions()
.iter()
.map(|s| s.to_string())
.collect(),
})
})
.collect()
}
#[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_table_next_cell_fallthrough() {
let args = vec![json!("Paragraph.\n"), json!(0), json!(3)];
let result = execute_command(COMMAND_TABLE_NEXT_CELL, &args)
.unwrap()
.unwrap();
assert_eq!(result, json!({ "inTable": false, "position": null }));
}
#[test]
fn test_table_next_cell_moves() {
let args = vec![json!(" | Name | Score |\n"), json!(0), json!(7)];
let result = execute_command(COMMAND_TABLE_NEXT_CELL, &args)
.unwrap()
.unwrap();
assert_eq!(
result,
json!({ "inTable": true, "position": { "line": 0, "column": 13 } })
);
}
#[test]
fn test_table_previous_cell_moves() {
let args = vec![json!(" | Name | Score |\n"), json!(0), json!(14)];
let result = execute_command(COMMAND_TABLE_PREVIOUS_CELL, &args)
.unwrap()
.unwrap();
assert_eq!(
result,
json!({ "inTable": true, "position": { "line": 0, "column": 6 } })
);
}
#[test]
fn test_table_navigation_missing_args() {
let result = execute_command(COMMAND_TABLE_NEXT_CELL, &[]);
assert!(result.is_err());
}
#[test]
fn test_formats_list_includes_core_formats() {
let result = execute_command(COMMAND_FORMATS_LIST, &[]).unwrap().unwrap();
let array = result.as_array().expect("formats list should be an array");
let names: Vec<&str> = array
.iter()
.map(|entry| entry.get("name").and_then(|n| n.as_str()).unwrap())
.collect();
assert!(names.contains(&"lex"));
assert!(names.contains(&"markdown"));
assert!(names.contains(&"html"));
let markdown = array
.iter()
.find(|entry| entry.get("name").and_then(|n| n.as_str()) == Some("markdown"))
.unwrap();
assert_eq!(markdown.get("supportsParsing").unwrap(), &json!(true));
assert_eq!(markdown.get("supportsSerialization").unwrap(), &json!(true));
let extensions = markdown.get("fileExtensions").unwrap().as_array().unwrap();
assert!(!extensions.is_empty());
}
#[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"));
}
}