use crate::domain::error::{DomainError, DomainResult};
use serde_json::Value;
use std::sync::Arc;
use tower_lsp::jsonrpc::Result as LspResult;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};
use tracing::{debug, error, info, instrument, warn};
use crate::lsp::domain::{CompletionContext, CompletionQuery};
use crate::lsp::error::LspError;
use crate::lsp::services::{CommandService, CompletionService, DocumentService};
#[derive(Debug, Clone)]
pub struct BkmrConfig {
pub bkmr_binary: String,
pub max_completions: usize,
pub enable_interpolation: bool,
}
impl Default for BkmrConfig {
fn default() -> Self {
Self {
bkmr_binary: "bkmr".to_string(),
max_completions: 50,
enable_interpolation: true,
}
}
}
#[derive(Debug)]
pub struct BkmrLspBackend {
client: Client,
config: BkmrConfig,
completion_service: CompletionService,
document_service: DocumentService,
command_service: CommandService,
}
impl BkmrLspBackend {
pub fn with_services(
client: Client,
config: BkmrConfig,
completion_service: CompletionService,
document_service: DocumentService,
command_service: CommandService,
) -> Self {
Self {
client,
config,
completion_service,
document_service,
command_service,
}
}
#[instrument(skip(self))]
fn extract_snippet_query(&self, uri: &Url, position: Position) -> Option<(String, Range)> {
self.document_service
.extract_snippet_query_sync(uri, position)
}
fn get_language_id(&self, uri: &Url) -> Option<String> {
self.document_service.get_language_id_sync(uri)
}
#[instrument(skip(self))]
async fn verify_bkmr_availability(&self) -> DomainResult<()> {
debug!("Verifying bkmr availability");
let command_future = tokio::process::Command::new(&self.config.bkmr_binary)
.args(["--help"])
.output();
let output =
match tokio::time::timeout(std::time::Duration::from_secs(5), command_future).await {
Ok(Ok(output)) => output,
Ok(Err(e)) => {
return Err(DomainError::Other(format!("bkmr binary not found: {}", e)));
}
Err(_) => {
return Err(DomainError::Other(
"bkmr --help command timed out".to_string(),
));
}
};
if !output.status.success() {
return Err(DomainError::Other(
"bkmr binary is not working properly".to_string(),
));
}
info!("bkmr binary verified successfully");
Ok(())
}
}
#[tower_lsp::async_trait]
impl LanguageServer for BkmrLspBackend {
#[instrument(skip(self, params))]
async fn initialize(&self, params: InitializeParams) -> LspResult<InitializeResult> {
info!(
"Initialize request received from client: {:?}",
params.client_info
);
if let Err(e) = self.verify_bkmr_availability().await {
error!("bkmr verification failed: {}", e);
self.client
.log_message(
MessageType::ERROR,
&format!("Failed to verify bkmr availability: {}", e),
)
.await;
}
let snippet_support = params
.capabilities
.text_document
.as_ref()
.and_then(|td| td.completion.as_ref())
.and_then(|comp| comp.completion_item.as_ref())
.and_then(|item| item.snippet_support)
.unwrap_or(false);
info!("Client snippet support: {}", snippet_support);
if !snippet_support {
warn!("Client does not support snippets");
self.client
.log_message(
MessageType::WARNING,
"Client does not support snippets, functionality may be limited",
)
.await;
}
let result = InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::FULL,
)),
completion_provider: Some(CompletionOptions {
resolve_provider: Some(false),
trigger_characters: None, all_commit_characters: None,
work_done_progress_options: WorkDoneProgressOptions::default(),
completion_item: None,
}),
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec![
"bkmr.insertFilepathComment".to_string(),
"bkmr.createSnippet".to_string(),
"bkmr.listSnippets".to_string(),
"bkmr.getSnippet".to_string(),
"bkmr.updateSnippet".to_string(),
"bkmr.deleteSnippet".to_string(),
],
work_done_progress_options: WorkDoneProgressOptions::default(),
}),
..Default::default()
},
server_info: Some(ServerInfo {
name: "bkmr-lsp".to_string(),
version: Some(env!("CARGO_PKG_VERSION").to_string()),
}),
};
info!("Initialize complete - manual completion only (no trigger characters)");
Ok(result)
}
#[instrument(skip(self))]
async fn initialized(&self, _: InitializedParams) {
info!("Server initialized successfully");
self.client
.log_message(MessageType::INFO, "bkmr-lsp server ready")
.await;
}
#[instrument(skip(self))]
async fn shutdown(&self) -> LspResult<()> {
info!("Shutdown request received");
self.client
.log_message(MessageType::INFO, "Shutting down bkmr-lsp server")
.await;
Ok(())
}
#[instrument(skip(self, params))]
async fn did_open(&self, params: DidOpenTextDocumentParams) {
let uri = params.text_document.uri.to_string();
let content = params.text_document.text;
let language_id = params.text_document.language_id;
debug!("Document opened: {} (language: {})", uri, language_id);
if let Err(e) = self
.document_service
.open_document(uri, language_id, content)
.await
{
error!("Failed to open document: {}", e);
}
}
#[instrument(skip(self, params))]
async fn did_change(&self, params: DidChangeTextDocumentParams) {
let uri = params.text_document.uri.to_string();
debug!("Document changed: {}", uri);
for change in params.content_changes {
if change.range.is_none() {
if let Err(e) = self
.document_service
.update_document(uri.clone(), change.text)
.await
{
error!("Failed to update document: {}", e);
}
} else {
if let Err(e) = self
.document_service
.update_document(uri.clone(), change.text)
.await
{
error!("Failed to update document: {}", e);
}
}
}
}
#[instrument(skip(self, params))]
async fn did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri.to_string();
debug!("Document closed: {}", uri);
if let Err(e) = self.document_service.close_document(uri).await {
error!("Failed to close document: {}", e);
}
}
#[instrument(skip(self, params))]
async fn completion(&self, params: CompletionParams) -> LspResult<Option<CompletionResponse>> {
let uri = ¶ms.text_document_position.text_document.uri;
let position = params.text_document_position.position;
debug!(
"Completion request for {}:{},{}",
uri, position.line, position.character
);
if let Some(context) = ¶ms.context {
match context.trigger_kind {
CompletionTriggerKind::INVOKED => {
debug!("Manual completion request - proceeding with word-based snippet search");
}
CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS => {
debug!("Completion for incomplete results - proceeding");
}
_ => {
debug!("Ignoring automatic trigger - only manual completion supported");
return Ok(Some(CompletionResponse::Array(vec![])));
}
}
} else {
debug!("No completion context - skipping");
return Ok(Some(CompletionResponse::Array(vec![])));
}
let query_info = self.extract_snippet_query(uri, position);
debug!("Extracted snippet query info: {:?}", query_info);
let language_id = self.get_language_id(uri);
debug!("Document language ID: {:?}", language_id);
let mut context = CompletionContext::new(uri.clone(), position, language_id);
if let Some((query, range)) = query_info {
debug!("Query: '{}', Range: {:?}", query, range);
context = context.with_query(CompletionQuery::new(query, range));
} else {
debug!("No query extracted, using empty query");
}
match self.completion_service.get_completions(&context).await {
Ok(completion_items) => {
info!(
"Returning {} completion items for query: {:?}",
completion_items.len(),
context.get_query_text().unwrap_or("")
);
for (i, item) in completion_items.iter().enumerate().take(3) {
debug!(
"Item {}: label='{}', sort_text={:?}",
i, item.label, item.sort_text
);
}
if completion_items.len() > 3 {
debug!("... and {} more items", completion_items.len() - 3);
}
Ok(Some(CompletionResponse::List(CompletionList {
is_incomplete: true,
items: completion_items,
})))
}
Err(e) => {
error!("Failed to get completions: {}", e);
self.client
.log_message(
MessageType::ERROR,
&format!("Failed to get completions: {}", e),
)
.await;
Ok(Some(CompletionResponse::Array(vec![])))
}
}
}
#[instrument(skip(self, params))]
async fn execute_command(&self, params: ExecuteCommandParams) -> LspResult<Option<Value>> {
debug!("Execute command request: {}", params.command);
match params.command.as_str() {
"bkmr.insertFilepathComment" => {
if let Some(arg) = params.arguments.first() {
if let Some(uri_str) = arg.as_str() {
debug!("Executing insertFilepathComment for URI: {}", uri_str);
match CommandService::insert_filepath_comment(uri_str) {
Ok(workspace_edit) => {
match self.client.apply_edit(workspace_edit).await {
Ok(response) => {
if response.applied {
info!("Successfully applied filepath comment edit");
return Ok(Some(serde_json::json!({"success": true})));
} else {
error!(
"Client failed to apply edit: {:?}",
response.failure_reason
);
return Ok(Some(serde_json::json!({
"success": false,
"error": response.failure_reason.unwrap_or_else(|| "Unknown error".to_string())
})));
}
}
Err(e) => {
error!("Failed to send workspace edit to client: {}", e);
return Ok(Some(serde_json::json!({
"success": false,
"error": format!("Failed to apply edit: {}", e)
})));
}
}
}
Err(e) => {
error!("Failed to create filepath comment edit: {}", e);
return Ok(Some(serde_json::json!({
"success": false,
"error": format!("Failed to create edit: {}", e)
})));
}
}
} else {
error!("Invalid argument format for insertFilepathComment");
return Ok(Some(serde_json::json!({
"success": false,
"error": "Invalid argument format"
})));
}
} else {
error!("No arguments provided for insertFilepathComment command");
return Ok(Some(serde_json::json!({
"success": false,
"error": "No arguments provided"
})));
}
}
"bkmr.createSnippet" => {
if let Some(arg) = params.arguments.first() {
let url = arg.get("url").and_then(|v| v.as_str());
let title = arg.get("title").and_then(|v| v.as_str());
let description = arg.get("description").and_then(|v| v.as_str());
let tags = arg
.get("tags")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<_>>()
})
.unwrap_or_default();
if let (Some(url), Some(title)) = (url, title) {
match self
.command_service
.create_snippet(url, title, description, tags)
{
Ok(result) => Ok(Some(result)),
Err(e) => Ok(Some(e.to_lsp_response())),
}
} else {
Ok(Some(
LspError::InvalidInput(
"Missing required fields: url and title".to_string(),
)
.to_lsp_response(),
))
}
} else {
Ok(Some(
LspError::InvalidInput("No arguments provided".to_string())
.to_lsp_response(),
))
}
}
"bkmr.listSnippets" => {
let language_id = params
.arguments
.first()
.and_then(|arg| arg.get("language"))
.and_then(|v| v.as_str());
match self.command_service.list_snippets(language_id) {
Ok(result) => Ok(Some(result)),
Err(e) => Ok(Some(e.to_lsp_response())),
}
}
"bkmr.getSnippet" => {
if let Some(arg) = params.arguments.first() {
if let Some(id) = arg.get("id").and_then(|v| v.as_i64()) {
match self.command_service.get_snippet(id as i32) {
Ok(result) => Ok(Some(result)),
Err(e) => Ok(Some(e.to_lsp_response())),
}
} else {
Ok(Some(
LspError::InvalidInput("Missing or invalid id parameter".to_string())
.to_lsp_response(),
))
}
} else {
Ok(Some(
LspError::InvalidInput("No arguments provided".to_string())
.to_lsp_response(),
))
}
}
"bkmr.updateSnippet" => {
if let Some(arg) = params.arguments.first() {
let id = arg.get("id").and_then(|v| v.as_i64()).map(|i| i as i32);
let url = arg.get("url").and_then(|v| v.as_str());
let title = arg.get("title").and_then(|v| v.as_str());
let description = arg.get("description").and_then(|v| v.as_str());
let tags = arg.get("tags").and_then(|v| v.as_array()).map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect::<Vec<_>>()
});
if let Some(id) = id {
match self
.command_service
.update_snippet(id, url, title, description, tags)
{
Ok(result) => Ok(Some(result)),
Err(e) => Ok(Some(e.to_lsp_response())),
}
} else {
Ok(Some(
LspError::InvalidInput("Missing required field: id".to_string())
.to_lsp_response(),
))
}
} else {
Ok(Some(
LspError::InvalidInput("No arguments provided".to_string())
.to_lsp_response(),
))
}
}
"bkmr.deleteSnippet" => {
if let Some(arg) = params.arguments.first() {
if let Some(id) = arg.get("id").and_then(|v| v.as_i64()) {
match self.command_service.delete_snippet(id as i32) {
Ok(result) => Ok(Some(result)),
Err(e) => Ok(Some(e.to_lsp_response())),
}
} else {
Ok(Some(
LspError::InvalidInput("Missing or invalid id parameter".to_string())
.to_lsp_response(),
))
}
} else {
Ok(Some(
LspError::InvalidInput("No arguments provided".to_string())
.to_lsp_response(),
))
}
}
_ => {
warn!("Unknown command: {}", params.command);
Ok(Some(serde_json::json!({
"success": false,
"error": format!("Unknown command: {}", params.command)
})))
}
}
}
}
pub async fn run_server(settings: &crate::config::Settings, no_interpolation: bool) {
let version = env!("CARGO_PKG_VERSION");
info!("Starting bkmr LSP server v{}", version);
let config = BkmrConfig {
bkmr_binary: "bkmr".to_string(),
max_completions: 50,
enable_interpolation: !no_interpolation,
};
info!("Configuration: {:?}", config);
if let Err(e) = validate_environment().await {
error!("Environment validation failed: {}", e);
std::process::exit(1);
}
use crate::infrastructure::di::ServiceContainer;
let service_container =
ServiceContainer::new(settings).expect("Failed to create service container");
let (service, socket) = LspService::new({
let config = config.clone();
let service_container = service_container;
move |client| {
use crate::lsp::services::{
CommandService, CompletionService, DocumentService, LspSnippetService,
};
let snippet_service = Arc::new(LspSnippetService::with_services(
service_container.bookmark_service.clone(),
service_container.interpolation_service.clone(),
));
let completion_service = CompletionService::new(snippet_service);
let document_service = DocumentService::new();
let command_service =
CommandService::with_service(service_container.bookmark_service.clone());
BkmrLspBackend::with_services(
client,
config,
completion_service,
document_service,
command_service,
)
}
});
info!("LSP service created, starting server on stdin/stdout");
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
info!("Starting LSP server loop");
Server::new(stdin, stdout, socket).serve(service).await;
info!("Server shutdown gracefully");
}
async fn validate_environment() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use std::io::IsTerminal;
if std::io::stdin().is_terminal() || std::io::stdout().is_terminal() {
eprintln!("Warning: bkmr lsp is designed to run as an LSP server");
eprintln!("It should be launched by an LSP client, not directly from a terminal");
eprintln!("If you're testing, pipe some LSP messages to stdin");
}
tokio::time::timeout(std::time::Duration::from_millis(100), async {
tokio::task::yield_now().await
})
.await?;
Ok(())
}