use dashmap::DashMap; use kdl::{KdlDocument, KdlError}; use miette::Diagnostic as _; use ropey::Rope; use tower_lsp::jsonrpc::Result; use tower_lsp::lsp_types::*; use tower_lsp::{Client, LanguageServer, LspService, Server}; use tracing_subscriber::prelude::*; use tracing_subscriber::EnvFilter; #[derive(Debug)] struct Backend { client: Client, document_map: DashMap, } impl Backend { async fn on_change(&self, uri: Url, text: &str) { let rope = ropey::Rope::from_str(text); self.document_map.insert(uri.to_string(), rope.clone()); } } #[tower_lsp::async_trait] impl LanguageServer for Backend { async fn initialize(&self, _: InitializeParams) -> Result { Ok(InitializeResult { capabilities: ServerCapabilities { text_document_sync: Some(TextDocumentSyncCapability::Options( TextDocumentSyncOptions { open_close: Some(true), change: Some(TextDocumentSyncKind::FULL), save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { include_text: Some(true), })), ..Default::default() }, )), workspace: Some(WorkspaceServerCapabilities { workspace_folders: Some(WorkspaceFoldersServerCapabilities { supported: Some(true), change_notifications: Some(OneOf::Left(true)), }), file_operations: None, }), diagnostic_provider: Some(DiagnosticServerCapabilities::RegistrationOptions( DiagnosticRegistrationOptions { text_document_registration_options: TextDocumentRegistrationOptions { document_selector: Some(vec![DocumentFilter { language: Some("kdl".into()), scheme: Some("file".into()), pattern: None, }]), }, ..Default::default() }, )), // hover_provider: Some(HoverProviderCapability::Simple(true)), // completion_provider: Some(Default::default()), ..Default::default() }, ..Default::default() }) } async fn initialized(&self, _: InitializedParams) { self.client .log_message(MessageType::INFO, "server initialized!") .await; } async fn shutdown(&self) -> Result<()> { self.client .log_message(MessageType::INFO, "server shutting down") .await; Ok(()) } async fn did_open(&self, params: DidOpenTextDocumentParams) { self.on_change(params.text_document.uri, ¶ms.text_document.text) .await; } async fn did_change(&self, params: DidChangeTextDocumentParams) { self.on_change(params.text_document.uri, ¶ms.content_changes[0].text) .await; } async fn did_save(&self, params: DidSaveTextDocumentParams) { if let Some(text) = params.text.as_ref() { self.on_change(params.text_document.uri, text).await; } } async fn did_close(&self, params: DidCloseTextDocumentParams) { self.document_map .remove(¶ms.text_document.uri.to_string()); } async fn diagnostic( &self, params: DocumentDiagnosticParams, ) -> Result { tracing::debug!("diagnostic req"); if let Some(doc) = self.document_map.get(¶ms.text_document.uri.to_string()) { let res: std::result::Result = doc.to_string().parse(); if let Err(kdl_err) = res { let diags = kdl_err .diagnostics .into_iter() .map(|diag| { Diagnostic::new( Range::new( char_to_position(diag.span.offset(), &doc), char_to_position(diag.span.offset() + diag.span.len(), &doc), ), diag.severity().map(to_lsp_sev), diag.code().map(|c| NumberOrString::String(c.to_string())), None, diag.to_string(), None, None, ) }) .collect(); return Ok(DocumentDiagnosticReportResult::Report( DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { related_documents: None, full_document_diagnostic_report: FullDocumentDiagnosticReport { result_id: None, items: diags, }, }), )); } } Ok(DocumentDiagnosticReportResult::Report( DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()), )) } // TODO(@zkat): autocomplete #-keywords // TODO(@zkat): autocomplete schema stuff // async fn completion(&self, _: CompletionParams) -> Result> { // tracing::debug!("Completion request"); // Ok(Some(CompletionResponse::Array(vec![ // CompletionItem::new_simple("Hello".to_string(), "Some detail".to_string()), // CompletionItem::new_simple("Bye".to_string(), "More detail".to_string()), // ]))) // } // TODO(@zkat): We'll use this when we actually do schema stuff. // async fn hover(&self, _: HoverParams) -> Result> { // tracing::debug!("Hover request"); // Ok(Some(Hover { // contents: HoverContents::Scalar(MarkedString::String("You're hovering!".to_string())), // range: None, // })) // } } fn char_to_position(char_idx: usize, rope: &Rope) -> Position { let line_idx = rope.char_to_line(char_idx); let line_char_idx = rope.line_to_char(line_idx); let column_idx = char_idx - line_char_idx; Position::new(line_idx as u32, column_idx as u32) } fn to_lsp_sev(sev: miette::Severity) -> DiagnosticSeverity { match sev { miette::Severity::Advice => DiagnosticSeverity::HINT, miette::Severity::Warning => DiagnosticSeverity::WARNING, miette::Severity::Error => DiagnosticSeverity::ERROR, } } #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::fmt::layer() .map_writer(move |_| std::io::stderr) .with_ansi(false), ) .with(EnvFilter::from_default_env()) .init(); let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); let (service, socket) = LspService::new(|client| Backend { client, document_map: DashMap::new(), }); Server::new(stdin, stdout, socket).serve(service).await; }