1use std::collections::HashMap;
2use std::sync::Mutex;
3
4use tower_lsp::jsonrpc::Result;
5use tower_lsp::lsp_types::*;
6use tower_lsp::{Client, LanguageServer, LspService, Server};
7
8use crate::format::Format;
9use crate::{FormatConfig, format_text};
10
11pub struct SnapperLsp {
12 client: Client,
13 documents: Mutex<HashMap<Url, (String, Format)>>,
14}
15
16impl SnapperLsp {
17 fn new(client: Client) -> Self {
18 Self {
19 client,
20 documents: Mutex::new(HashMap::new()),
21 }
22 }
23
24 fn make_config(&self, format: Format) -> FormatConfig {
25 FormatConfig {
26 format,
27 max_width: 0,
28 use_neural: false,
29 neural_lang: "en".to_string(),
30 neural_model_path: None,
31 extra_abbreviations: vec![],
32 use_pandoc: false,
33 pandoc_format: None,
34 }
35 }
36
37 fn format_document(&self, uri: &Url) -> Option<Vec<TextEdit>> {
38 let docs = self.documents.lock().ok()?;
39 let (text, format) = docs.get(uri)?;
40 let config = self.make_config(*format);
41 let formatted = format_text(text, &config).ok()?;
42 if formatted == *text {
43 return None;
44 }
45 let lines = text.lines().count();
46 let last_line_len = text.lines().last().map_or(0, |l| l.len());
47 Some(vec![TextEdit {
48 range: Range {
49 start: Position::new(0, 0),
50 end: Position::new(lines as u32, last_line_len as u32),
51 },
52 new_text: formatted,
53 }])
54 }
55
56 fn compute_diagnostics(&self, uri: &Url) -> Vec<Diagnostic> {
57 let docs = self.documents.lock().ok().unwrap();
58 let Some((text, _)) = docs.get(uri) else {
59 return vec![];
60 };
61
62 let mut diagnostics = Vec::new();
63 for (i, line) in text.lines().enumerate() {
64 let trimmed = line.trim();
66 if trimmed.is_empty() {
67 continue;
68 }
69 let mut sentence_boundaries = 0;
70 let chars: Vec<char> = trimmed.chars().collect();
71 for j in 1..chars.len().saturating_sub(1) {
72 if (chars[j - 1] == '.' || chars[j - 1] == '!' || chars[j - 1] == '?')
73 && chars[j] == ' '
74 && chars.get(j + 1).is_some_and(|c| c.is_uppercase())
75 {
76 sentence_boundaries += 1;
77 }
78 }
79 if sentence_boundaries >= 1 {
80 diagnostics.push(Diagnostic {
81 range: Range {
82 start: Position::new(i as u32, 0),
83 end: Position::new(i as u32, line.len() as u32),
84 },
85 severity: Some(DiagnosticSeverity::HINT),
86 source: Some("snapper".to_string()),
87 message: format!(
88 "Line contains {} sentence boundary(ies). Consider running snapper.",
89 sentence_boundaries
90 ),
91 ..Default::default()
92 });
93 }
94 }
95 diagnostics
96 }
97}
98
99#[tower_lsp::async_trait]
100impl LanguageServer for SnapperLsp {
101 async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
102 Ok(InitializeResult {
103 capabilities: ServerCapabilities {
104 text_document_sync: Some(TextDocumentSyncCapability::Kind(
105 TextDocumentSyncKind::FULL,
106 )),
107 document_formatting_provider: Some(OneOf::Left(true)),
108 document_range_formatting_provider: Some(OneOf::Left(true)),
109 ..Default::default()
110 },
111 ..Default::default()
112 })
113 }
114
115 async fn initialized(&self, _: InitializedParams) {
116 self.client
117 .log_message(MessageType::INFO, "snapper LSP initialized")
118 .await;
119 }
120
121 async fn shutdown(&self) -> Result<()> {
122 Ok(())
123 }
124
125 async fn did_open(&self, params: DidOpenTextDocumentParams) {
126 let uri = params.text_document.uri.clone();
127 let text = params.text_document.text.clone();
128 let format = detect_format_from_uri(&uri, ¶ms.text_document.language_id);
129 self.documents
130 .lock()
131 .unwrap()
132 .insert(uri.clone(), (text, format));
133
134 let diagnostics = self.compute_diagnostics(&uri);
135 self.client
136 .publish_diagnostics(uri, diagnostics, None)
137 .await;
138 }
139
140 async fn did_change(&self, params: DidChangeTextDocumentParams) {
141 let uri = params.text_document.uri.clone();
142 if let Some(change) = params.content_changes.into_iter().last() {
143 let format = {
144 let docs = self.documents.lock().unwrap();
145 docs.get(&uri).map_or(Format::Plaintext, |(_, f)| *f)
146 };
147 self.documents
148 .lock()
149 .unwrap()
150 .insert(uri.clone(), (change.text, format));
151
152 let diagnostics = self.compute_diagnostics(&uri);
153 self.client
154 .publish_diagnostics(uri, diagnostics, None)
155 .await;
156 }
157 }
158
159 async fn did_close(&self, params: DidCloseTextDocumentParams) {
160 self.documents
161 .lock()
162 .unwrap()
163 .remove(¶ms.text_document.uri);
164 }
165
166 async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
167 Ok(self.format_document(¶ms.text_document.uri))
168 }
169
170 async fn range_formatting(
171 &self,
172 params: DocumentRangeFormattingParams,
173 ) -> Result<Option<Vec<TextEdit>>> {
174 let uri = ¶ms.text_document.uri;
175 let range = params.range;
176 let docs = self.documents.lock().unwrap();
177 let Some((text, format)) = docs.get(uri) else {
178 return Ok(None);
179 };
180
181 let lines: Vec<&str> = text.lines().collect();
182 let start = range.start.line as usize;
183 let end = (range.end.line as usize).min(lines.len().saturating_sub(1));
184 let range_text = lines[start..=end].join("\n");
185
186 let config = self.make_config(*format);
187 let formatted = match format_text(&range_text, &config) {
188 Ok(f) => f,
189 Err(_) => return Ok(None),
190 };
191
192 if formatted == range_text {
193 return Ok(None);
194 }
195
196 let last_col = lines.get(end).map_or(0, |l| l.len());
197
198 Ok(Some(vec![TextEdit {
199 range: Range {
200 start: Position::new(start as u32, 0),
201 end: Position::new(end as u32, last_col as u32),
202 },
203 new_text: formatted,
204 }]))
205 }
206}
207
208fn detect_format_from_uri(uri: &Url, language_id: &str) -> Format {
209 match language_id {
211 "org" => return Format::Org,
212 "latex" | "tex" => return Format::Latex,
213 "markdown" => return Format::Markdown,
214 "plaintext" => return Format::Plaintext,
215 _ => {}
216 }
217 if let Ok(path) = uri.to_file_path() {
219 Format::from_path(&path)
220 } else {
221 Format::Plaintext
222 }
223}
224
225pub async fn run_lsp() {
227 let stdin = tokio::io::stdin();
228 let stdout = tokio::io::stdout();
229
230 let (service, socket) = LspService::new(SnapperLsp::new);
231 Server::new(stdin, stdout, socket).serve(service).await;
232}