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