use crate::analysis::completion::CompletionEngine;
use crate::analysis::diagnostic::DiagnosticEngine;
use crate::analysis::rust::macro_analyzer::MacroAnalyzer;
use crate::analysis::toml::toml_analyzer::TomlAnalyzer;
use crate::core::config::ServerConfig;
use crate::core::document::DocumentManager;
use crate::core::index::IndexManager;
use crate::core::schema::SchemaProvider;
use crate::scanner::route::RouteNavigator;
use crate::utils::error::{ErrorHandler, RecoveryAction};
use crate::utils::status::ServerStatus;
use crate::{Error, Result};
use lsp_server::{Connection, Message, Notification, Request, RequestId, Response};
use lsp_types::{
notification::{
DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, Exit, Notification as _,
},
request::{Completion, DocumentSymbolRequest, GotoDefinition, HoverRequest, Request as _},
CompletionParams, CompletionResponse, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams,
GotoDefinitionResponse, HoverParams, InitializeParams, InitializeResult, ServerCapabilities,
ServerInfo,
};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerState {
Uninitialized,
Initialized,
ShuttingDown,
}
pub struct LspServer {
connection: Connection,
pub state: ServerState,
pub workspace_path: Option<std::path::PathBuf>,
pub document_manager: Arc<DocumentManager>,
error_handler: ErrorHandler,
pub config: ServerConfig,
pub status: ServerStatus,
pub schema_provider: Arc<SchemaProvider>,
pub toml_analyzer: Arc<TomlAnalyzer>,
pub macro_analyzer: Arc<MacroAnalyzer>,
pub route_navigator: Arc<RouteNavigator>,
pub completion_engine: Arc<CompletionEngine>,
pub diagnostic_engine: Arc<DiagnosticEngine>,
pub index_manager: Arc<IndexManager>,
}
impl LspServer {
pub fn start() -> Result<Self> {
tracing::info!("Starting summer-lsp server");
let (connection, _io_threads) = Connection::stdio();
Self::new_with_connection(connection)
}
pub fn new_for_test() -> Result<Self> {
let (connection, _io_threads) = Connection::memory();
Self::new_with_connection(connection)
}
fn new_with_connection(connection: Connection) -> Result<Self> {
let config = ServerConfig::load(None);
if let Err(e) = config.validate() {
tracing::error!("Invalid configuration: {}", e);
return Err(Error::Config(e));
}
let verbose = config.logging.verbose;
tracing::info!("Initializing components...");
tracing::info!("Loading configuration schema...");
let schema_provider = Arc::new({
let runtime = tokio::runtime::Runtime::new()
.map_err(|e| Error::SchemaLoad(format!("Failed to create tokio runtime: {}", e)))?;
match runtime.block_on(SchemaProvider::load()) {
Ok(provider) => {
tracing::info!("Schema loaded successfully from URL");
provider
}
Err(e) => {
tracing::warn!("Failed to load schema from URL: {}, using fallback", e);
SchemaProvider::default()
}
}
});
let toml_analyzer = Arc::new(TomlAnalyzer::new((*schema_provider).clone()));
let macro_analyzer = Arc::new(MacroAnalyzer::new());
let route_navigator = Arc::new(RouteNavigator::new());
let completion_engine = Arc::new(CompletionEngine::new((*schema_provider).clone()));
let diagnostic_engine = Arc::new(DiagnosticEngine::new());
let index_manager = Arc::new(IndexManager::new());
tracing::info!("All components initialized successfully");
Ok(Self {
connection,
state: ServerState::Uninitialized,
workspace_path: None,
document_manager: Arc::new(DocumentManager::new()),
error_handler: ErrorHandler::new(verbose),
config,
status: ServerStatus::new(),
schema_provider,
toml_analyzer,
macro_analyzer,
route_navigator,
completion_engine,
diagnostic_engine,
index_manager,
})
}
pub fn run(&mut self) -> Result<()> {
self.initialize()?;
self.event_loop()?;
self.shutdown()?;
Ok(())
}
fn initialize(&mut self) -> Result<()> {
tracing::info!("Waiting for initialize request");
let (id, params) = self.connection.initialize_start()?;
let init_params: InitializeParams = serde_json::from_value(params)?;
tracing::info!(
"Received initialize request from client: {:?}",
init_params.client_info
);
let init_result = self.handle_initialize(init_params)?;
let init_result_json = serde_json::to_value(init_result)?;
self.connection.initialize_finish(id, init_result_json)?;
self.state = ServerState::Initialized;
tracing::info!("LSP server initialized successfully");
Ok(())
}
fn event_loop(&mut self) -> Result<()> {
tracing::info!("Entering main event loop");
loop {
if self.state == ServerState::ShuttingDown {
tracing::info!("Server is shutting down, stopping event loop");
break;
}
let msg = match self.connection.receiver.recv() {
Ok(msg) => msg,
Err(e) => {
let error = Error::MessageReceive(e.to_string());
let result = self.error_handler.handle(&error);
match result.action {
RecoveryAction::RetryConnection => {
tracing::info!("Attempting to recover connection...");
std::thread::sleep(std::time::Duration::from_millis(100));
continue;
}
RecoveryAction::Abort => {
tracing::error!("Fatal error receiving message, shutting down");
break;
}
_ => {
tracing::warn!("Unexpected recovery action for message receive error");
break;
}
}
}
};
if let Err(e) = self.handle_message(msg) {
self.status.record_error();
let result = self.error_handler.handle(&e);
match result.action {
RecoveryAction::Abort => {
tracing::error!("Fatal error, shutting down server");
self.state = ServerState::ShuttingDown;
break;
}
_ => {
if result.notify_client {
if let Err(notify_err) = self.notify_client_error(&e) {
tracing::error!(
"Failed to notify client about error: {}",
notify_err
);
}
}
}
}
}
}
Ok(())
}
fn handle_message(&mut self, msg: Message) -> Result<()> {
match msg {
Message::Request(req) => self.handle_request(req),
Message::Response(resp) => {
tracing::debug!("Received response: {:?}", resp.id);
Ok(())
}
Message::Notification(not) => self.handle_notification(not),
}
}
fn handle_request(&mut self, req: Request) -> Result<()> {
tracing::debug!("Received request: {} (id: {:?})", req.method, req.id);
self.status.record_request();
if self.connection.handle_shutdown(&req)? {
tracing::info!("Received shutdown request");
self.state = ServerState::ShuttingDown;
return Ok(());
}
match req.method.as_str() {
Completion::METHOD => self.handle_completion(req),
HoverRequest::METHOD => self.handle_hover(req),
GotoDefinition::METHOD => self.handle_goto_definition(req),
DocumentSymbolRequest::METHOD => self.handle_document_symbol(req),
"workspace/symbol" => self.handle_workspace_symbol(req),
"summer-lsp/status" => self.handle_status_query(req),
"summer/components" => self.handle_components_request(req),
"summer/routes" => self.handle_routes_request(req),
"summer/jobs" => self.handle_jobs_request(req),
"summer/plugins" => self.handle_plugins_request(req),
"summer/configurations" => self.handle_configurations_request(req),
_ => {
tracing::warn!("Unhandled request method: {}", req.method);
self.send_error_response(
req.id,
lsp_server::ErrorCode::MethodNotFound as i32,
format!("Method not found: {}", req.method),
)
}
}?;
Ok(())
}
fn handle_notification(&mut self, not: Notification) -> Result<()> {
tracing::debug!("Received notification: {}", not.method);
match not.method.as_str() {
DidOpenTextDocument::METHOD => {
let params: DidOpenTextDocumentParams = serde_json::from_value(not.params)?;
self.handle_did_open(params)?;
}
DidChangeTextDocument::METHOD => {
let params: DidChangeTextDocumentParams = serde_json::from_value(not.params)?;
self.handle_did_change(params)?;
}
DidCloseTextDocument::METHOD => {
let params: DidCloseTextDocumentParams = serde_json::from_value(not.params)?;
self.handle_did_close(params)?;
}
Exit::METHOD => {
tracing::info!("Received exit notification");
self.state = ServerState::ShuttingDown;
}
_ => {
tracing::debug!("Unhandled notification method: {}", not.method);
}
}
Ok(())
}
pub fn handle_did_open(&mut self, params: DidOpenTextDocumentParams) -> Result<()> {
let doc = params.text_document;
tracing::debug!("Document opened: {}", doc.uri);
self.document_manager.open(
doc.uri.clone(),
doc.version,
doc.text,
doc.language_id.clone(),
);
self.status.increment_document_count();
self.analyze_document(&doc.uri, &doc.language_id)?;
Ok(())
}
pub fn handle_did_change(&mut self, params: DidChangeTextDocumentParams) -> Result<()> {
let uri = params.text_document.uri;
let version = params.text_document.version;
tracing::debug!("Document changed: {} (version: {})", uri, version);
self.document_manager
.change(&uri, version, params.content_changes);
if let Some(doc) = self.document_manager.get(&uri) {
self.analyze_document(&uri, &doc.language_id)?;
}
Ok(())
}
pub fn handle_did_close(&mut self, params: DidCloseTextDocumentParams) -> Result<()> {
let uri = params.text_document.uri;
tracing::info!("Document closed: {}", uri);
self.document_manager.close(&uri);
self.status.decrement_document_count();
self.diagnostic_engine.clear(&uri);
let _ = self.diagnostic_engine.publish(&self.connection, &uri);
Ok(())
}
fn handle_completion(&mut self, req: Request) -> Result<()> {
tracing::debug!("Handling completion request");
let params: CompletionParams = serde_json::from_value(req.params)?;
self.status.record_completion();
let response = self.document_manager.with_document(
¶ms.text_document_position.text_document.uri,
|doc| {
match doc.language_id.as_str() {
"toml" => {
if let Ok(toml_doc) = self.toml_analyzer.parse(&doc.content) {
self.completion_engine.complete_toml_document(
&toml_doc,
params.text_document_position.position,
)
} else {
vec![]
}
}
"rust" => {
vec![]
}
_ => vec![],
}
},
);
let result = match response {
Some(completions) => serde_json::to_value(CompletionResponse::Array(completions))?,
None => serde_json::Value::Null,
};
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_hover(&mut self, req: Request) -> Result<()> {
tracing::debug!("Handling hover request");
let params: HoverParams = serde_json::from_value(req.params)?;
self.status.record_hover();
let response = self.document_manager.with_document(
¶ms.text_document_position_params.text_document.uri,
|doc| {
match doc.language_id.as_str() {
"toml" => {
if let Ok(toml_doc) = self.toml_analyzer.parse(&doc.content) {
self.toml_analyzer
.hover(&toml_doc, params.text_document_position_params.position)
} else {
None
}
}
"rust" => {
None
}
_ => None,
}
},
);
let result = match response {
Some(Some(hover)) => serde_json::to_value(hover)?,
_ => serde_json::Value::Null,
};
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_goto_definition(&mut self, req: Request) -> Result<()> {
tracing::debug!("Handling goto definition request");
let _params: GotoDefinitionParams = serde_json::from_value(req.params)?;
let result = GotoDefinitionResponse::Array(vec![]);
let response = Response {
id: req.id,
result: Some(serde_json::to_value(result)?),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
pub fn analyze_document(&mut self, uri: &lsp_types::Url, language_id: &str) -> Result<()> {
tracing::debug!("Analyzing document: {} ({})", uri, language_id);
self.diagnostic_engine.clear(uri);
let diagnostics = self
.document_manager
.with_document(uri, |doc| {
match language_id {
"toml" => {
match self.toml_analyzer.parse(&doc.content) {
Ok(toml_doc) => {
let mut diagnostics = Vec::new();
let validation_diagnostics = self.toml_analyzer.validate(&toml_doc);
diagnostics.extend(validation_diagnostics);
diagnostics
}
Err(e) => {
tracing::error!("TOML parse error: {}", e);
vec![lsp_types::Diagnostic {
range: lsp_types::Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 0,
},
},
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"parse_error".to_string(),
)),
code_description: None,
source: Some("summer-lsp".to_string()),
message: format!("TOML parse error: {}", e),
related_information: None,
tags: None,
data: None,
}]
}
}
}
"rust" => {
vec![]
}
_ => {
tracing::debug!("Unsupported language: {}", language_id);
vec![]
}
}
})
.unwrap_or_default();
let filtered_diagnostics: Vec<_> = diagnostics
.into_iter()
.filter(|diag| {
if let Some(lsp_types::NumberOrString::String(code)) = &diag.code {
!self.config.diagnostics.is_disabled(code)
} else {
true
}
})
.collect();
for diagnostic in filtered_diagnostics {
self.diagnostic_engine.add(uri.clone(), diagnostic);
}
let _ = self.diagnostic_engine.publish(&self.connection, uri);
self.status.record_diagnostic();
Ok(())
}
fn handle_status_query(&self, req: Request) -> Result<()> {
tracing::debug!("Handling status query request");
let metrics = self.status.get_metrics();
let result = serde_json::to_value(metrics)?;
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_routes_request(&self, req: Request) -> Result<()> {
tracing::info!("Handling summer/routes request");
use crate::scanner::route::{RouteScanner, RoutesRequest, RoutesResponse};
let params: RoutesRequest = serde_json::from_value(req.params)?;
let project_path = std::path::Path::new(¶ms.app_path);
tracing::info!("Scanning routes in: {:?}", project_path);
let scanner = RouteScanner::new();
let routes = match scanner.scan_routes(project_path) {
Ok(routes) => {
tracing::info!("Successfully scanned {} routes", routes.len());
routes
}
Err(e) => {
tracing::error!("Failed to scan routes: {}", e);
Vec::new()
}
};
let response_data = RoutesResponse { routes };
tracing::info!(
"Sending response with {} routes",
response_data.routes.len()
);
let result = serde_json::to_value(response_data)?;
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_components_request(&self, req: Request) -> Result<()> {
tracing::info!("Handling summer/components request");
use crate::scanner::component::{ComponentScanner, ComponentsRequest, ComponentsResponse};
let params: ComponentsRequest = serde_json::from_value(req.params)?;
let project_path = std::path::Path::new(¶ms.app_path);
tracing::info!("Scanning components in: {:?}", project_path);
tracing::info!("Project path exists: {}", project_path.exists());
tracing::info!("Project path is dir: {}", project_path.is_dir());
let scanner = ComponentScanner::new();
let components = match scanner.scan_components(project_path) {
Ok(components) => {
tracing::info!("Successfully scanned {} components", components.len());
components
}
Err(e) => {
tracing::error!("Failed to scan components: {}", e);
tracing::error!("Error details: {:?}", e);
Vec::new()
}
};
let response_data = ComponentsResponse { components };
tracing::info!(
"Sending response with {} components",
response_data.components.len()
);
let result = serde_json::to_value(response_data)?;
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_jobs_request(&self, req: Request) -> Result<()> {
tracing::info!("Handling summer/jobs request");
use crate::scanner::job::{JobScanner, JobsRequest, JobsResponse};
let params: JobsRequest = serde_json::from_value(req.params)?;
let project_path = std::path::Path::new(¶ms.app_path);
tracing::info!("Scanning jobs in: {:?}", project_path);
let scanner = JobScanner::new();
let jobs = match scanner.scan_jobs(project_path) {
Ok(jobs) => {
tracing::info!("Successfully scanned {} jobs", jobs.len());
jobs
}
Err(e) => {
tracing::error!("Failed to scan jobs: {}", e);
Vec::new()
}
};
let response_data = JobsResponse { jobs };
tracing::info!("Sending response with {} jobs", response_data.jobs.len());
let result = serde_json::to_value(response_data)?;
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_plugins_request(&self, req: Request) -> Result<()> {
tracing::info!("Handling summer/plugins request");
use crate::scanner::plugin::{PluginScanner, PluginsRequest, PluginsResponse};
let params: PluginsRequest = serde_json::from_value(req.params)?;
let project_path = std::path::Path::new(¶ms.app_path);
tracing::info!("Scanning plugins in: {:?}", project_path);
let scanner = PluginScanner::new();
let plugins = match scanner.scan_plugins(project_path) {
Ok(plugins) => {
tracing::info!("Successfully scanned {} plugins", plugins.len());
plugins
}
Err(e) => {
tracing::error!("Failed to scan plugins: {}", e);
Vec::new()
}
};
let response_data = PluginsResponse { plugins };
tracing::info!(
"Sending response with {} plugins",
response_data.plugins.len()
);
let result = serde_json::to_value(response_data)?;
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_configurations_request(&self, req: Request) -> Result<()> {
tracing::debug!("Handling summer/configurations request");
use crate::scanner::config::{
ConfigScanner, ConfigurationsRequest, ConfigurationsResponse,
};
let params: ConfigurationsRequest = serde_json::from_value(req.params)?;
let project_path = std::path::Path::new(¶ms.app_path);
let scanner = ConfigScanner::new();
let configurations = match scanner.scan_configurations(project_path) {
Ok(configurations) => configurations,
Err(e) => {
tracing::error!("Failed to scan configurations: {}", e);
Vec::new()
}
};
let response_data = ConfigurationsResponse { configurations };
let result = serde_json::to_value(response_data)?;
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_document_symbol(&self, req: Request) -> Result<()> {
tracing::debug!("Handling textDocument/documentSymbol request");
let params: DocumentSymbolParams = serde_json::from_value(req.params)?;
let uri = ¶ms.text_document.uri;
let symbols = self.document_manager.with_document(uri, |doc| {
match doc.language_id.as_str() {
"toml" => {
self.extract_toml_symbols(&doc.content)
}
"rust" => {
self.extract_rust_symbols(&doc.content)
}
_ => {
tracing::debug!(
"Unsupported language for document symbols: {}",
doc.language_id
);
vec![]
}
}
});
let result = match symbols {
Some(symbols) => serde_json::to_value(DocumentSymbolResponse::Nested(symbols))?,
None => serde_json::Value::Null,
};
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn handle_workspace_symbol(&self, req: Request) -> Result<()> {
tracing::debug!("Handling workspace/symbol request");
let params: serde_json::Value = req.params;
let query = params
.get("query")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_lowercase();
tracing::debug!("Workspace symbol query: '{}'", query);
let mut symbols: Vec<lsp_types::SymbolInformation> = vec![];
if let Some(workspace_path) = &self.workspace_path {
if let Ok(component_symbols) = self.search_component_symbols(workspace_path, &query) {
symbols.extend(component_symbols);
}
if let Ok(route_symbols) = self.search_route_symbols(workspace_path, &query) {
symbols.extend(route_symbols);
}
if let Ok(config_symbols) = self.search_config_symbols(workspace_path, &query) {
symbols.extend(config_symbols);
}
}
tracing::debug!(
"Found {} workspace symbols matching '{}'",
symbols.len(),
query
);
let result = serde_json::to_value(symbols)?;
let response = Response {
id: req.id,
result: Some(result),
error: None,
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn search_component_symbols(
&self,
workspace_path: &std::path::Path,
query: &str,
) -> Result<Vec<lsp_types::SymbolInformation>> {
use crate::scanner::component::{ComponentScanner, ComponentSource};
use lsp_types::{Location, Position, Range, SymbolInformation, SymbolKind, Url};
let scanner = ComponentScanner::new();
let components = scanner
.scan_components(workspace_path)
.map_err(|e| Error::Other(anyhow::anyhow!("Failed to scan components: {}", e)))?;
let mut symbols = Vec::new();
for component in components {
if !query.is_empty() && !component.name.to_lowercase().contains(query) {
continue;
}
let uri = Url::parse(&component.location.uri)
.map_err(|e| Error::Other(anyhow::anyhow!("Invalid URI: {}", e)))?;
let range = Range {
start: Position {
line: component.location.range.start.line,
character: component.location.range.start.character,
},
end: Position {
line: component.location.range.end.line,
character: component.location.range.end.character,
},
};
let kind = match component.source {
ComponentSource::Component => SymbolKind::METHOD,
ComponentSource::Service => SymbolKind::CLASS,
};
#[allow(deprecated)]
symbols.push(SymbolInformation {
name: component.name.clone(),
kind,
tags: None,
deprecated: None,
location: Location { uri, range },
container_name: Some(format!("Component ({})", component.type_name)),
});
}
Ok(symbols)
}
fn search_route_symbols(
&self,
workspace_path: &std::path::Path,
query: &str,
) -> Result<Vec<lsp_types::SymbolInformation>> {
use crate::scanner::route::RouteScanner;
use lsp_types::{Location, Position, Range, SymbolInformation, SymbolKind, Url};
let scanner = RouteScanner::new();
let routes = scanner
.scan_routes(workspace_path)
.map_err(|e| Error::Other(anyhow::anyhow!("Failed to scan routes: {}", e)))?;
let mut symbols = Vec::new();
for route in routes {
let search_text = format!("{} {}", route.method, route.path).to_lowercase();
if !query.is_empty() && !search_text.contains(query) {
continue;
}
let uri = Url::parse(&route.location.uri)
.map_err(|e| Error::Other(anyhow::anyhow!("Invalid URI: {}", e)))?;
let range = Range {
start: Position {
line: route.location.range.start.line,
character: route.location.range.start.character,
},
end: Position {
line: route.location.range.end.line,
character: route.location.range.end.character,
},
};
#[allow(deprecated)]
symbols.push(SymbolInformation {
name: format!("{} {}", route.method, route.path),
kind: SymbolKind::FUNCTION,
tags: None,
deprecated: None,
location: Location { uri, range },
container_name: Some(format!("Route ({})", route.handler)),
});
}
Ok(symbols)
}
fn search_config_symbols(
&self,
workspace_path: &std::path::Path,
query: &str,
) -> Result<Vec<lsp_types::SymbolInformation>> {
use crate::scanner::config::ConfigScanner;
use lsp_types::{SymbolInformation, SymbolKind};
let scanner = ConfigScanner::new();
let configs = scanner
.scan_configurations(workspace_path)
.map_err(|e| Error::Other(anyhow::anyhow!("Failed to scan configurations: {}", e)))?;
let mut symbols = Vec::new();
for config in configs {
if !query.is_empty() && !config.name.to_lowercase().contains(query) {
continue;
}
if let Some(location) = config.location {
#[allow(deprecated)]
symbols.push(SymbolInformation {
name: config.name.clone(),
kind: SymbolKind::STRUCT,
tags: None,
deprecated: None,
location,
container_name: Some(format!("Config [{}]", config.prefix)),
});
}
}
Ok(symbols)
}
fn extract_toml_symbols(&self, content: &str) -> Vec<lsp_types::DocumentSymbol> {
use lsp_types::{DocumentSymbol, Position, Range, SymbolKind};
let mut symbols = Vec::new();
let parse_result = taplo::parser::parse(content);
let root = parse_result.into_dom();
if let taplo::dom::Node::Table(table) = root {
let entries = table.entries();
let entries_arc = entries.get();
for (key, value) in entries_arc.iter() {
let key_str = key.value().to_string();
let key_range = Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: key_str.len() as u32,
},
};
match value {
taplo::dom::Node::Table(inner_table) => {
let mut children = Vec::new();
let inner_entries = inner_table.entries();
let inner_entries_arc = inner_entries.get();
for (prop_key, _prop_value) in inner_entries_arc.iter() {
let prop_key_str = prop_key.value().to_string();
let prop_symbol = DocumentSymbol {
name: prop_key_str.clone(),
detail: Some("Property".to_string()),
kind: SymbolKind::PROPERTY,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: key_range,
selection_range: key_range,
children: None,
};
children.push(prop_symbol);
}
let symbol = DocumentSymbol {
name: key_str.clone(),
detail: Some("Configuration section".to_string()),
kind: SymbolKind::MODULE,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: key_range,
selection_range: key_range,
children: if children.is_empty() {
None
} else {
Some(children)
},
};
symbols.push(symbol);
}
taplo::dom::Node::Array(_) => {
let symbol = DocumentSymbol {
name: key_str.clone(),
detail: Some("Array".to_string()),
kind: SymbolKind::ARRAY,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: key_range,
selection_range: key_range,
children: None,
};
symbols.push(symbol);
}
_ => {
let symbol = DocumentSymbol {
name: key_str.clone(),
detail: Some("Property".to_string()),
kind: SymbolKind::PROPERTY,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: key_range,
selection_range: key_range,
children: None,
};
symbols.push(symbol);
}
}
}
}
symbols
}
fn extract_rust_symbols(&self, _content: &str) -> Vec<lsp_types::DocumentSymbol> {
use lsp_types::{DocumentSymbol, Range, SymbolKind};
let mut symbols = Vec::new();
let syntax = match syn::parse_file(_content) {
Ok(syntax) => syntax,
Err(e) => {
tracing::warn!("Failed to parse Rust file: {}", e);
return symbols;
}
};
let default_range = Range::default();
for item in syntax.items {
match item {
syn::Item::Fn(item_fn) => {
let name = item_fn.sig.ident.to_string();
let symbol = DocumentSymbol {
name: name.clone(),
detail: Some(format!("fn {}", name)),
kind: SymbolKind::FUNCTION,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: default_range,
selection_range: default_range,
children: None,
};
symbols.push(symbol);
}
syn::Item::Struct(item_struct) => {
let name = item_struct.ident.to_string();
let symbol = DocumentSymbol {
name: name.clone(),
detail: Some(format!("struct {}", name)),
kind: SymbolKind::STRUCT,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: default_range,
selection_range: default_range,
children: None,
};
symbols.push(symbol);
}
syn::Item::Enum(item_enum) => {
let name = item_enum.ident.to_string();
let symbol = DocumentSymbol {
name: name.clone(),
detail: Some(format!("enum {}", name)),
kind: SymbolKind::ENUM,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: default_range,
selection_range: default_range,
children: None,
};
symbols.push(symbol);
}
syn::Item::Trait(item_trait) => {
let name = item_trait.ident.to_string();
let symbol = DocumentSymbol {
name: name.clone(),
detail: Some(format!("trait {}", name)),
kind: SymbolKind::INTERFACE,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: default_range,
selection_range: default_range,
children: None,
};
symbols.push(symbol);
}
syn::Item::Impl(item_impl) => {
if let Some((_, path, _)) = &item_impl.trait_ {
let name = quote::quote!(#path).to_string();
let symbol = DocumentSymbol {
name: format!("impl {}", name),
detail: Some("Implementation".to_string()),
kind: SymbolKind::CLASS,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: default_range,
selection_range: default_range,
children: None,
};
symbols.push(symbol);
} else if let syn::Type::Path(type_path) = &*item_impl.self_ty {
let name = quote::quote!(#type_path).to_string();
let symbol = DocumentSymbol {
name: format!("impl {}", name),
detail: Some("Implementation".to_string()),
kind: SymbolKind::CLASS,
tags: None,
#[allow(deprecated)]
deprecated: None,
range: default_range,
selection_range: default_range,
children: None,
};
symbols.push(symbol);
}
}
_ => {
}
}
}
symbols
}
pub fn handle_initialize(&mut self, params: InitializeParams) -> Result<InitializeResult> {
use lsp_types::{
CompletionOptions, HoverProviderCapability, OneOf, TextDocumentSyncCapability,
TextDocumentSyncKind, TextDocumentSyncOptions, WorkDoneProgressOptions,
};
#[allow(deprecated)]
if let Some(root_uri) = params.root_uri {
if let Ok(workspace_path) = root_uri.to_file_path() {
tracing::info!(
"Loading configuration from workspace: {}",
workspace_path.display()
);
self.workspace_path = Some(workspace_path.clone());
self.config = ServerConfig::load(Some(&workspace_path));
if let Err(e) = self.config.validate() {
tracing::error!("Invalid configuration: {}", e);
return Err(Error::Config(e));
}
tracing::info!("Configuration loaded successfully");
tracing::debug!(
"Trigger characters: {:?}",
self.config.completion.trigger_characters
);
tracing::debug!("Schema URL: {}", self.config.schema.url);
tracing::debug!(
"Disabled diagnostics: {:?}",
self.config.diagnostics.disabled
);
}
}
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
open_close: Some(true),
change: Some(TextDocumentSyncKind::INCREMENTAL),
will_save: None,
will_save_wait_until: None,
save: None,
},
)),
completion_provider: Some(CompletionOptions {
resolve_provider: Some(true),
trigger_characters: Some(self.config.completion.trigger_characters.clone()),
all_commit_characters: None,
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: None,
},
completion_item: None,
}),
hover_provider: Some(HoverProviderCapability::Simple(true)),
definition_provider: Some(OneOf::Left(true)),
document_symbol_provider: Some(OneOf::Left(true)),
workspace_symbol_provider: Some(OneOf::Left(true)),
..Default::default()
},
server_info: Some(ServerInfo {
name: "summer-lsp".to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
})
}
fn send_error_response(&self, id: RequestId, code: i32, message: String) -> Result<()> {
let response = Response {
id,
result: None,
error: Some(lsp_server::ResponseError {
code,
message,
data: None,
}),
};
self.connection
.sender
.send(Message::Response(response))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
fn notify_client_error(&self, error: &Error) -> Result<()> {
use lsp_types::{MessageType, ShowMessageParams};
let message_type = match error.severity() {
crate::error::ErrorSeverity::Error => MessageType::ERROR,
crate::error::ErrorSeverity::Warning => MessageType::WARNING,
crate::error::ErrorSeverity::Info => MessageType::INFO,
};
let params = ShowMessageParams {
typ: message_type,
message: error.to_string(),
};
let notification = Notification {
method: "window/showMessage".to_string(),
params: serde_json::to_value(params)?,
};
self.connection
.sender
.send(Message::Notification(notification))
.map_err(|e| Error::MessageSend(e.to_string()))?;
Ok(())
}
pub fn shutdown(&mut self) -> Result<()> {
tracing::info!("Shutting down summer-lsp server");
tracing::debug!("Clearing all diagnostics...");
tracing::debug!("Clearing document cache...");
tracing::debug!("Clearing indexes...");
tracing::info!("Server shutdown complete");
Ok(())
}
}
impl Default for LspServer {
fn default() -> Self {
Self::start().expect("Failed to start LSP server")
}
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::{
ClientCapabilities, ClientInfo, InitializeParams, TextDocumentItem, Url,
VersionedTextDocumentIdentifier, WorkDoneProgressParams,
};
#[test]
fn test_server_state_transitions() {
let server = LspServer::new_for_test().unwrap();
assert_eq!(server.state, ServerState::Uninitialized);
}
#[test]
fn test_document_open() {
let mut server = LspServer::new_for_test().unwrap();
server.state = ServerState::Initialized;
let uri = Url::parse("file:///test.toml").unwrap();
let params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "toml".to_string(),
version: 1,
text: "host = \"localhost\"".to_string(),
},
};
server.handle_did_open(params).unwrap();
let doc = server.document_manager.get(&uri);
assert!(doc.is_some());
let doc = doc.unwrap();
assert_eq!(doc.version, 1);
assert_eq!(doc.content, "host = \"localhost\"");
assert_eq!(doc.language_id, "toml");
}
#[test]
fn test_document_change() {
let mut server = LspServer::new_for_test().unwrap();
server.state = ServerState::Initialized;
let uri = Url::parse("file:///test.toml").unwrap();
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "toml".to_string(),
version: 1,
text: "host = \"localhost\"".to_string(),
},
};
server.handle_did_open(open_params).unwrap();
let change_params = DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: uri.clone(),
version: 2,
},
content_changes: vec![lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "host = \"127.0.0.1\"".to_string(),
}],
};
server.handle_did_change(change_params).unwrap();
let doc = server.document_manager.get(&uri).unwrap();
assert_eq!(doc.version, 2);
assert_eq!(doc.content, "host = \"127.0.0.1\"");
}
#[test]
fn test_document_close() {
let mut server = LspServer::new_for_test().unwrap();
server.state = ServerState::Initialized;
let uri = Url::parse("file:///test.toml").unwrap();
let open_params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "toml".to_string(),
version: 1,
text: "host = \"localhost\"".to_string(),
},
};
server.handle_did_open(open_params).unwrap();
assert!(server.document_manager.get(&uri).is_some());
let close_params = DidCloseTextDocumentParams {
text_document: lsp_types::TextDocumentIdentifier { uri: uri.clone() },
};
server.handle_did_close(close_params).unwrap();
assert!(server.document_manager.get(&uri).is_none());
}
#[test]
fn test_initialize_response() {
let mut server = LspServer::new_for_test().unwrap();
#[allow(deprecated)]
let params = InitializeParams {
process_id: Some(1234),
root_uri: None,
capabilities: ClientCapabilities::default(),
client_info: Some(ClientInfo {
name: "test-client".to_string(),
version: Some("1.0.0".to_string()),
}),
locale: None,
root_path: None,
initialization_options: None,
trace: None,
workspace_folders: Some(vec![lsp_types::WorkspaceFolder {
uri: Url::parse("file:///workspace").unwrap(),
name: "workspace".to_string(),
}]),
work_done_progress_params: WorkDoneProgressParams::default(),
};
let result = server.handle_initialize(params).unwrap();
assert!(result.server_info.is_some());
let server_info = result.server_info.unwrap();
assert_eq!(server_info.name, "summer-lsp");
assert!(server_info.version.is_some());
let capabilities = result.capabilities;
assert!(capabilities.text_document_sync.is_some());
if let Some(lsp_types::TextDocumentSyncCapability::Options(sync_options)) =
capabilities.text_document_sync
{
assert_eq!(sync_options.open_close, Some(true));
assert_eq!(
sync_options.change,
Some(lsp_types::TextDocumentSyncKind::INCREMENTAL)
);
} else {
panic!("Expected TextDocumentSyncOptions");
}
assert!(capabilities.completion_provider.is_some());
let completion = capabilities.completion_provider.unwrap();
assert_eq!(completion.resolve_provider, Some(true));
assert!(completion.trigger_characters.is_some());
let triggers = completion.trigger_characters.unwrap();
assert!(triggers.contains(&"[".to_string()));
assert!(triggers.contains(&"$".to_string()));
assert!(triggers.contains(&"{".to_string()));
assert!(capabilities.hover_provider.is_some());
assert!(capabilities.definition_provider.is_some());
assert!(capabilities.document_symbol_provider.is_some());
assert!(capabilities.workspace_symbol_provider.is_some());
}
#[test]
fn test_error_recovery() {
let mut server = LspServer::new_for_test().unwrap();
server.state = ServerState::Initialized;
let uri = Url::parse("file:///nonexistent.toml").unwrap();
let change_params = DidChangeTextDocumentParams {
text_document: VersionedTextDocumentIdentifier {
uri: uri.clone(),
version: 1,
},
content_changes: vec![lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: "test".to_string(),
}],
};
let result = server.handle_did_change(change_params);
assert!(result.is_ok());
assert!(server.document_manager.get(&uri).is_none());
}
}