|
| 1 | +use dashmap::DashMap; |
| 2 | +use kdl::{KdlDocument, KdlError}; |
| 3 | +use miette::Diagnostic as _; |
| 4 | +use ropey::Rope; |
| 5 | +use tower_lsp::jsonrpc::Result; |
| 6 | +use tower_lsp::lsp_types::*; |
| 7 | +use tower_lsp::{Client, LanguageServer, LspService, Server}; |
| 8 | +use tracing_subscriber::prelude::*; |
| 9 | +use tracing_subscriber::EnvFilter; |
| 10 | + |
| 11 | +#[derive(Debug)] |
| 12 | +struct Backend { |
| 13 | + client: Client, |
| 14 | + document_map: DashMap<String, Rope>, |
| 15 | +} |
| 16 | + |
| 17 | +impl Backend { |
| 18 | + async fn on_change(&self, uri: Url, text: &str) { |
| 19 | + let rope = ropey::Rope::from_str(text); |
| 20 | + self.document_map.insert(uri.to_string(), rope.clone()); |
| 21 | + } |
| 22 | +} |
| 23 | + |
| 24 | +#[tower_lsp::async_trait] |
| 25 | +impl LanguageServer for Backend { |
| 26 | + async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> { |
| 27 | + Ok(InitializeResult { |
| 28 | + capabilities: ServerCapabilities { |
| 29 | + text_document_sync: Some(TextDocumentSyncCapability::Options( |
| 30 | + TextDocumentSyncOptions { |
| 31 | + open_close: Some(true), |
| 32 | + change: Some(TextDocumentSyncKind::FULL), |
| 33 | + save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { |
| 34 | + include_text: Some(true), |
| 35 | + })), |
| 36 | + ..Default::default() |
| 37 | + }, |
| 38 | + )), |
| 39 | + workspace: Some(WorkspaceServerCapabilities { |
| 40 | + workspace_folders: Some(WorkspaceFoldersServerCapabilities { |
| 41 | + supported: Some(true), |
| 42 | + change_notifications: Some(OneOf::Left(true)), |
| 43 | + }), |
| 44 | + file_operations: None, |
| 45 | + }), |
| 46 | + diagnostic_provider: Some(DiagnosticServerCapabilities::RegistrationOptions( |
| 47 | + DiagnosticRegistrationOptions { |
| 48 | + text_document_registration_options: TextDocumentRegistrationOptions { |
| 49 | + document_selector: Some(vec![DocumentFilter { |
| 50 | + language: Some("kdl".into()), |
| 51 | + scheme: Some("file".into()), |
| 52 | + pattern: None, |
| 53 | + }]), |
| 54 | + }, |
| 55 | + ..Default::default() |
| 56 | + }, |
| 57 | + )), |
| 58 | + // hover_provider: Some(HoverProviderCapability::Simple(true)), |
| 59 | + // completion_provider: Some(Default::default()), |
| 60 | + ..Default::default() |
| 61 | + }, |
| 62 | + ..Default::default() |
| 63 | + }) |
| 64 | + } |
| 65 | + |
| 66 | + async fn initialized(&self, _: InitializedParams) { |
| 67 | + self.client |
| 68 | + .log_message(MessageType::INFO, "server initialized!") |
| 69 | + .await; |
| 70 | + } |
| 71 | + |
| 72 | + async fn shutdown(&self) -> Result<()> { |
| 73 | + self.client |
| 74 | + .log_message(MessageType::INFO, "server shutting down") |
| 75 | + .await; |
| 76 | + Ok(()) |
| 77 | + } |
| 78 | + |
| 79 | + async fn did_open(&self, params: DidOpenTextDocumentParams) { |
| 80 | + self.on_change(params.text_document.uri, ¶ms.text_document.text) |
| 81 | + .await; |
| 82 | + } |
| 83 | + |
| 84 | + async fn did_change(&self, params: DidChangeTextDocumentParams) { |
| 85 | + self.on_change(params.text_document.uri, ¶ms.content_changes[0].text) |
| 86 | + .await; |
| 87 | + } |
| 88 | + |
| 89 | + async fn did_save(&self, params: DidSaveTextDocumentParams) { |
| 90 | + if let Some(text) = params.text.as_ref() { |
| 91 | + self.on_change(params.text_document.uri, text).await; |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | + async fn did_close(&self, params: DidCloseTextDocumentParams) { |
| 96 | + self.document_map |
| 97 | + .remove(¶ms.text_document.uri.to_string()); |
| 98 | + } |
| 99 | + |
| 100 | + async fn diagnostic( |
| 101 | + &self, |
| 102 | + params: DocumentDiagnosticParams, |
| 103 | + ) -> Result<DocumentDiagnosticReportResult> { |
| 104 | + tracing::debug!("diagnostic req"); |
| 105 | + if let Some(doc) = self.document_map.get(¶ms.text_document.uri.to_string()) { |
| 106 | + let res: std::result::Result<KdlDocument, KdlError> = doc.to_string().parse(); |
| 107 | + if let Err(kdl_err) = res { |
| 108 | + let diags = kdl_err |
| 109 | + .diagnostics |
| 110 | + .into_iter() |
| 111 | + .map(|diag| { |
| 112 | + Diagnostic::new( |
| 113 | + Range::new( |
| 114 | + char_to_position(diag.span.offset(), &doc), |
| 115 | + char_to_position(diag.span.offset() + diag.span.len(), &doc), |
| 116 | + ), |
| 117 | + diag.severity().map(to_lsp_sev), |
| 118 | + diag.code().map(|c| NumberOrString::String(c.to_string())), |
| 119 | + None, |
| 120 | + diag.to_string(), |
| 121 | + None, |
| 122 | + None, |
| 123 | + ) |
| 124 | + }) |
| 125 | + .collect(); |
| 126 | + return Ok(DocumentDiagnosticReportResult::Report( |
| 127 | + DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { |
| 128 | + related_documents: None, |
| 129 | + full_document_diagnostic_report: FullDocumentDiagnosticReport { |
| 130 | + result_id: None, |
| 131 | + items: diags, |
| 132 | + }, |
| 133 | + }), |
| 134 | + )); |
| 135 | + } |
| 136 | + } |
| 137 | + Ok(DocumentDiagnosticReportResult::Report( |
| 138 | + DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()), |
| 139 | + )) |
| 140 | + } |
| 141 | + |
| 142 | + // TODO(@zkat): autocomplete #-keywords |
| 143 | + // TODO(@zkat): autocomplete schema stuff |
| 144 | + // async fn completion(&self, _: CompletionParams) -> Result<Option<CompletionResponse>> { |
| 145 | + // tracing::debug!("Completion request"); |
| 146 | + // Ok(Some(CompletionResponse::Array(vec![ |
| 147 | + // CompletionItem::new_simple("Hello".to_string(), "Some detail".to_string()), |
| 148 | + // CompletionItem::new_simple("Bye".to_string(), "More detail".to_string()), |
| 149 | + // ]))) |
| 150 | + // } |
| 151 | + |
| 152 | + // TODO(@zkat): We'll use this when we actually do schema stuff. |
| 153 | + // async fn hover(&self, _: HoverParams) -> Result<Option<Hover>> { |
| 154 | + // tracing::debug!("Hover request"); |
| 155 | + // Ok(Some(Hover { |
| 156 | + // contents: HoverContents::Scalar(MarkedString::String("You're hovering!".to_string())), |
| 157 | + // range: None, |
| 158 | + // })) |
| 159 | + // } |
| 160 | +} |
| 161 | + |
| 162 | +fn char_to_position(char_idx: usize, rope: &Rope) -> Position { |
| 163 | + let line_idx = rope.char_to_line(char_idx); |
| 164 | + let line_char_idx = rope.line_to_char(line_idx); |
| 165 | + let column_idx = char_idx - line_char_idx; |
| 166 | + Position::new(line_idx as u32, column_idx as u32) |
| 167 | +} |
| 168 | + |
| 169 | +fn to_lsp_sev(sev: miette::Severity) -> DiagnosticSeverity { |
| 170 | + match sev { |
| 171 | + miette::Severity::Advice => DiagnosticSeverity::HINT, |
| 172 | + miette::Severity::Warning => DiagnosticSeverity::WARNING, |
| 173 | + miette::Severity::Error => DiagnosticSeverity::ERROR, |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +#[tokio::main] |
| 178 | +async fn main() { |
| 179 | + tracing_subscriber::registry() |
| 180 | + .with( |
| 181 | + tracing_subscriber::fmt::layer() |
| 182 | + .map_writer(move |_| std::io::stderr) |
| 183 | + .with_ansi(false), |
| 184 | + ) |
| 185 | + .with(EnvFilter::from_default_env()) |
| 186 | + .init(); |
| 187 | + |
| 188 | + let stdin = tokio::io::stdin(); |
| 189 | + let stdout = tokio::io::stdout(); |
| 190 | + |
| 191 | + let (service, socket) = LspService::new(|client| Backend { |
| 192 | + client, |
| 193 | + document_map: DashMap::new(), |
| 194 | + }); |
| 195 | + Server::new(stdin, stdout, socket).serve(service).await; |
| 196 | +} |
0 commit comments