mod analyzer;
mod basic;
mod capabilities;
mod formatter;
mod server;
pub use analyzer::SemanticAnalyzer;
use anyhow;
pub use basic::{LspServer, Notification, Request, Response};
pub use capabilities::{ruchy_token_to_lsp, RuchyTokenType, SEMANTIC_TOKEN_LEGEND};
pub use formatter::Formatter;
pub use server::RuchyLanguageServer;
use std::collections::HashMap;
use tokio::net::TcpListener;
use tower_lsp::lsp_types::Url;
use tower_lsp::{LspService, Server};
pub struct Workspace {
documents: HashMap<Url, String>,
}
impl Workspace {
pub fn new() -> Self {
Self {
documents: HashMap::new(),
}
}
pub fn add_document(&mut self, uri: Url, content: String) {
self.documents.insert(uri, content);
}
pub fn update_document(&mut self, uri: &Url, content: String) {
self.documents.insert(uri.clone(), content);
}
pub fn get_document(&self, uri: &Url) -> anyhow::Result<&String> {
self.documents
.get(uri)
.ok_or_else(|| anyhow::anyhow!("Document not found: {uri}"))
}
pub fn remove_document(&mut self, uri: &Url) {
self.documents.remove(uri);
}
}
impl Default for Workspace {
fn default() -> Self {
Self::new()
}
}
pub async fn start_server() -> anyhow::Result<()> {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let (service, socket) = LspService::new(RuchyLanguageServer::new);
Server::new(stdin, stdout, socket).serve(service).await;
Ok(())
}
pub async fn start_tcp_server(port: u16) -> anyhow::Result<()> {
let listener = TcpListener::bind(format!("127.0.0.1:{port}")).await?;
while let Ok((stream, _)) = listener.accept().await {
let (read, write) = tokio::io::split(stream);
let (service, socket) = LspService::new(RuchyLanguageServer::new);
tokio::spawn(async move {
Server::new(read, write, socket).serve(service).await;
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{
ruchy_token_to_lsp, Formatter, RuchyLanguageServer, RuchyTokenType, SemanticAnalyzer,
Workspace, SEMANTIC_TOKEN_LEGEND,
};
use tower_lsp::lsp_types::SemanticTokenType;
use tower_lsp::lsp_types::Url;
#[test]
fn test_workspace_creation() {
let workspace = Workspace::new();
assert!(workspace.documents.is_empty());
}
#[test]
fn test_workspace_default() {
let workspace = Workspace::default();
assert!(workspace.documents.is_empty());
}
#[test]
fn test_workspace_add_document() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///test.ruchy").expect("operation should succeed in test");
let content = "let x = 42".to_string();
workspace.add_document(uri.clone(), content.clone());
assert_eq!(workspace.documents.len(), 1);
assert_eq!(workspace.documents.get(&uri), Some(&content));
}
#[test]
fn test_workspace_update_document() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///test.ruchy").expect("operation should succeed in test");
workspace.add_document(uri.clone(), "initial".to_string());
workspace.update_document(&uri, "updated".to_string());
assert_eq!(workspace.documents.get(&uri), Some(&"updated".to_string()));
}
#[test]
fn test_workspace_get_document() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///test.ruchy").expect("operation should succeed in test");
let content = "let x = 42".to_string();
workspace.add_document(uri.clone(), content.clone());
let result = workspace.get_document(&uri);
assert!(result.is_ok());
assert_eq!(result.expect("operation should succeed in test"), &content);
}
#[test]
fn test_workspace_get_document_not_found() {
let workspace = Workspace::new();
let uri =
Url::parse("file:///nonexistent.ruchy").expect("operation should succeed in test");
let result = workspace.get_document(&uri);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Document not found"));
}
#[test]
fn test_workspace_remove_document() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///test.ruchy").expect("operation should succeed in test");
workspace.add_document(uri.clone(), "content".to_string());
assert_eq!(workspace.documents.len(), 1);
workspace.remove_document(&uri);
assert_eq!(workspace.documents.len(), 0);
}
#[test]
fn test_workspace_remove_nonexistent_document() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///test.ruchy").expect("operation should succeed in test");
workspace.remove_document(&uri);
assert_eq!(workspace.documents.len(), 0);
}
#[test]
fn test_workspace_multiple_documents() {
let mut workspace = Workspace::new();
let uri1 = Url::parse("file:///test1.ruchy").expect("operation should succeed in test");
let uri2 = Url::parse("file:///test2.ruchy").expect("operation should succeed in test");
let uri3 = Url::parse("file:///test3.ruchy").expect("operation should succeed in test");
workspace.add_document(uri1.clone(), "content1".to_string());
workspace.add_document(uri2.clone(), "content2".to_string());
workspace.add_document(uri3.clone(), "content3".to_string());
assert_eq!(workspace.documents.len(), 3);
assert_eq!(
workspace
.get_document(&uri1)
.expect("operation should succeed in test"),
"content1"
);
assert_eq!(
workspace
.get_document(&uri2)
.expect("operation should succeed in test"),
"content2"
);
assert_eq!(
workspace
.get_document(&uri3)
.expect("operation should succeed in test"),
"content3"
);
}
#[test]
fn test_workspace_update_overwrites() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///test.ruchy").expect("operation should succeed in test");
workspace.add_document(uri.clone(), "v1".to_string());
workspace.add_document(uri.clone(), "v2".to_string());
workspace.update_document(&uri, "v3".to_string());
assert_eq!(workspace.documents.len(), 1);
assert_eq!(
workspace
.get_document(&uri)
.expect("operation should succeed in test"),
"v3"
);
}
#[test]
fn test_workspace_with_different_schemes() {
let mut workspace = Workspace::new();
let file_uri = Url::parse("file:///test.ruchy").expect("operation should succeed in test");
let untitled_uri =
Url::parse("untitled:untitled-1").expect("operation should succeed in test");
workspace.add_document(file_uri.clone(), "file content".to_string());
workspace.add_document(untitled_uri.clone(), "untitled content".to_string());
assert_eq!(workspace.documents.len(), 2);
assert_eq!(
workspace
.get_document(&file_uri)
.expect("operation should succeed in test"),
"file content"
);
assert_eq!(
workspace
.get_document(&untitled_uri)
.expect("operation should succeed in test"),
"untitled content"
);
}
#[test]
fn test_workspace_empty_content() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///empty.ruchy").expect("operation should succeed in test");
workspace.add_document(uri.clone(), String::new());
assert_eq!(
workspace
.get_document(&uri)
.expect("operation should succeed in test"),
""
);
}
#[test]
fn test_workspace_large_content() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///large.ruchy").expect("operation should succeed in test");
let large_content = "x".repeat(10000);
workspace.add_document(uri.clone(), large_content.clone());
assert_eq!(
workspace
.get_document(&uri)
.expect("operation should succeed in test"),
&large_content
);
}
#[test]
fn test_semantic_analyzer_creation() {
let analyzer = SemanticAnalyzer::new();
let _ = analyzer;
}
#[test]
fn test_formatter_creation() {
let formatter = Formatter::new();
let _ = formatter;
}
#[test]
fn test_ruchy_language_server_type_exists() {
assert_eq!(
std::mem::size_of::<RuchyLanguageServer>(),
std::mem::size_of::<RuchyLanguageServer>()
);
}
#[test]
fn test_ruchy_token_type_variants() {
let _actor = RuchyTokenType::Actor as u32;
let _dataframe = RuchyTokenType::DataFrame as u32;
let _pipeline = RuchyTokenType::Pipeline as u32;
let _pattern = RuchyTokenType::Pattern as u32;
}
#[test]
fn test_semantic_token_legend() {
assert!(!SEMANTIC_TOKEN_LEGEND.token_types.is_empty());
assert!(SEMANTIC_TOKEN_LEGEND.token_types.len() >= 8); }
#[test]
fn test_ruchy_token_to_lsp_conversion() {
let actor_token = ruchy_token_to_lsp(RuchyTokenType::Actor);
let dataframe_token = ruchy_token_to_lsp(RuchyTokenType::DataFrame);
let pipeline_token = ruchy_token_to_lsp(RuchyTokenType::Pipeline);
let pattern_token = ruchy_token_to_lsp(RuchyTokenType::Pattern);
assert_eq!(actor_token, SemanticTokenType::CLASS);
assert_eq!(dataframe_token, SemanticTokenType::TYPE);
assert_eq!(pipeline_token, SemanticTokenType::OPERATOR);
assert_eq!(pattern_token, SemanticTokenType::ENUM_MEMBER);
}
#[test]
fn test_workspace_concurrent_updates() {
let mut workspace = Workspace::new();
let uri = Url::parse("file:///concurrent.ruchy").expect("operation should succeed in test");
for i in 0..10 {
workspace.update_document(&uri, format!("version {i}"));
}
assert_eq!(
workspace
.get_document(&uri)
.expect("operation should succeed in test"),
"version 9"
);
}
#[test]
fn test_workspace_uri_normalization() {
let mut workspace = Workspace::new();
let uri1 =
Url::parse("file:///home/user/test.ruchy").expect("operation should succeed in test");
let uri2 =
Url::parse("file:///home/user/test.ruchy").expect("operation should succeed in test");
workspace.add_document(uri1, "content".to_string());
assert_eq!(
workspace
.get_document(&uri2)
.expect("operation should succeed in test"),
"content"
);
}
}
#[cfg(test)]
mod property_tests_mod {
use proptest::prelude::*;
proptest! {
#[test]
fn test_new_never_panics(input: String) {
let _input = if input.len() > 100 { &input[..100] } else { &input[..] };
let _ = std::panic::catch_unwind(|| {
});
}
}
}