use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use dashmap::DashMap;
use tokio::sync::RwLock;
use tower_lsp::jsonrpc::Result as JsonRpcResult;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer};
use super::actions::compute_code_actions;
use super::commands::perform_split;
use super::config_watch::{find_workspace_config, reload_config};
use super::diagnostics::compute_file_diagnostics;
use super::hover::compute_hover;
use super::state::{DocumentState, WorkspaceState};
use crate::config::Config;
pub struct Backend {
pub client: Client,
pub documents: Arc<DashMap<Url, DocumentState>>,
pub workspaces: Arc<DashMap<Url, WorkspaceState>>,
pub config_cache: Arc<RwLock<HashMap<PathBuf, Config>>>,
}
impl Backend {
pub fn new(client: Client) -> Self {
Self {
client,
documents: Arc::new(DashMap::new()),
workspaces: Arc::new(DashMap::new()),
config_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
async fn get_config_for_uri(&self, uri: &Url) -> Config {
if let Some(config_path) = find_workspace_config(uri) {
{
let cache = self.config_cache.read().await;
if let Some(config) = cache.get(&config_path) {
return config.clone();
}
}
if let Ok(config) = reload_config(&config_path) {
let mut cache = self.config_cache.write().await;
cache.insert(config_path, config.clone());
return config;
}
}
Config::default()
}
async fn publish_diagnostics(&self, uri: &Url, text: &str) {
let config = self.get_config_for_uri(uri).await;
let diagnostics = match compute_file_diagnostics(text, &config) {
Ok(diags) => diags,
Err(e) => {
self.client
.log_message(MessageType::ERROR, format!("Diagnostics error: {e}"))
.await;
vec![]
}
};
self.client
.publish_diagnostics(uri.clone(), diagnostics, None)
.await;
}
}
#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> JsonRpcResult<InitializeResult> {
if let Some(folders) = ¶ms.workspace_folders {
for folder in folders {
let root = folder.uri.to_file_path().unwrap_or_default();
let config_path = find_workspace_config(&folder.uri);
self.workspaces
.insert(folder.uri.clone(), WorkspaceState { root, config_path });
}
}
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::FULL,
)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
code_action_provider: Some(CodeActionProviderCapability::Options(
CodeActionOptions {
code_action_kinds: Some(vec![CodeActionKind::REFACTOR_REWRITE]),
resolve_provider: Some(false),
work_done_progress_options: Default::default(),
},
)),
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec!["splitrs.split".into()],
work_done_progress_options: Default::default(),
}),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
file_operations: None,
}),
..Default::default()
},
server_info: Some(ServerInfo {
name: "splitrs-lsp".into(),
version: Some(env!("CARGO_PKG_VERSION").into()),
}),
})
}
async fn initialized(&self, _params: InitializedParams) {
self.client
.log_message(MessageType::INFO, "splitrs-lsp initialized")
.await;
}
async fn shutdown(&self) -> JsonRpcResult<()> {
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri.clone();
let text = params.text_document.text.clone();
let version = params.text_document.version;
self.documents
.insert(uri.clone(), DocumentState::new(text.clone(), version));
self.publish_diagnostics(&uri, &text).await;
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri.clone();
let version = params.text_document.version;
if let Some(change) = params.content_changes.into_iter().last() {
let text = change.text;
self.documents
.insert(uri.clone(), DocumentState::new(text.clone(), version));
self.publish_diagnostics(&uri, &text).await;
}
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
let uri = params.text_document.uri.clone();
if let Some(doc) = self.documents.get(&uri) {
let text = doc.text.clone();
drop(doc);
self.publish_diagnostics(&uri, &text).await;
}
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
self.documents.remove(¶ms.text_document.uri);
}
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
for change in params.changes {
if let Ok(path) = change.uri.to_file_path() {
if path.file_name().is_some_and(|n| n == ".splitrs.toml") {
let mut cache = self.config_cache.write().await;
cache.remove(&path);
}
}
}
}
async fn code_action(
&self,
params: CodeActionParams,
) -> JsonRpcResult<Option<CodeActionResponse>> {
let uri = ¶ms.text_document.uri;
let config = self.get_config_for_uri(uri).await;
let actions = compute_code_actions(uri, ¶ms, &config);
Ok(if actions.is_empty() {
None
} else {
Some(actions)
})
}
async fn execute_command(
&self,
params: ExecuteCommandParams,
) -> JsonRpcResult<Option<serde_json::Value>> {
if params.command != "splitrs.split" {
return Ok(None);
}
let uri_str = params
.arguments
.first()
.and_then(|v| v.get("uri"))
.and_then(|v| v.as_str())
.ok_or_else(|| tower_lsp::jsonrpc::Error {
code: tower_lsp::jsonrpc::ErrorCode::InvalidParams,
message: "Missing or invalid `uri` argument".into(),
data: None,
})?;
let uri = Url::parse(uri_str).map_err(|e| tower_lsp::jsonrpc::Error {
code: tower_lsp::jsonrpc::ErrorCode::InvalidParams,
message: e.to_string().into(),
data: None,
})?;
let text = match self.documents.get(&uri) {
Some(doc) => doc.text.clone(),
None => {
self.client
.show_message(MessageType::ERROR, "splitrs: document not open")
.await;
return Ok(None);
}
};
let config = self.get_config_for_uri(&uri).await;
match perform_split(&uri, &text, &config, None).await {
Ok(edit) => match self.client.apply_edit(edit).await {
Ok(_) => {
self.client
.show_message(MessageType::INFO, "splitrs: refactoring applied")
.await;
}
Err(e) => {
self.client
.show_message(
MessageType::ERROR,
format!("splitrs: failed to apply edit: {e}"),
)
.await;
}
},
Err(e) => {
self.client
.show_message(MessageType::ERROR, format!("splitrs: split failed: {e}"))
.await;
}
}
Ok(None)
}
async fn hover(&self, params: HoverParams) -> JsonRpcResult<Option<Hover>> {
let uri = ¶ms.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
let text = match self.documents.get(uri) {
Some(doc) => doc.text.clone(),
None => return Ok(None),
};
Ok(compute_hover(uri, position, &text))
}
}