1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Result, anyhow};
5use lsp_server::{Connection, Message, Notification, Response};
6use lsp_types::{
7 Position, PublishDiagnosticsParams, Uri,
8 notification::{
9 DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument,
10 Notification as LspNotification, PublishDiagnostics,
11 },
12 request::{
13 CodeActionRequest, Completion, DocumentDiagnosticRequest, DocumentHighlightRequest,
14 DocumentLinkRequest, DocumentSymbolRequest, FoldingRangeRequest, Formatting,
15 GotoDefinition, HoverRequest, RangeFormatting, References, Rename, Request as LspRequest,
16 WorkspaceDiagnosticRequest,
17 },
18};
19use serde::Deserialize;
20
21use crate::diagnostics::{self, DiagnosticLevel};
22use crate::formatter::ReflowMode;
23use crate::handlers;
24use crate::store::Store;
25use crate::tags::TagIndex;
26
27#[allow(clippy::struct_excessive_bools)]
28pub struct Config {
29 pub line_width: usize,
30 pub formatting: bool,
31 pub reflow: ReflowMode,
32 pub normalize_spacing: bool,
33 pub diagnostics: bool,
34 pub publish_diagnostics: bool,
35 pub hover: bool,
36 pub runtime_tags: bool,
37 pub tag_paths: Vec<PathBuf>,
38 pub diagnostic_levels: HashMap<String, DiagnosticLevel>,
39}
40
41#[derive(Deserialize, Default)]
42#[serde(rename_all = "camelCase")]
43pub struct InitOptions {
44 #[serde(default)]
45 pub tag_paths: Vec<PathBuf>,
46 pub runtime_tags: Option<bool>,
47 pub line_width: Option<usize>,
48 pub formatting: Option<bool>,
49 pub diagnostics: Option<bool>,
50 pub hover: Option<bool>,
51 pub reflow: Option<ReflowMode>,
52 pub normalize_spacing: Option<bool>,
53 #[serde(default)]
54 pub diagnostic_levels: HashMap<String, DiagnosticLevel>,
55}
56
57#[allow(clippy::missing_errors_doc)]
58pub fn main_loop(connection: &Connection, config: &Config, tag_index: &mut TagIndex) -> Result<()> {
59 let mut store = Store::default();
60
61 for msg in &connection.receiver {
62 match msg {
63 Message::Request(req) => {
64 if connection.handle_shutdown(&req)? {
65 return Ok(());
66 }
67 tracing::debug!(method = %req.method, "handling request");
68 let resp = handle_request(&req, &store, config, tag_index);
69 connection.sender.send(Message::Response(resp))?;
70 }
71 Message::Notification(notif) => {
72 tracing::debug!(method = %notif.method, "handling notification");
73 handle_notification(notif, &mut store, connection, config, tag_index)?;
74 }
75 Message::Response(_) => {}
76 }
77 }
78 Ok(())
79}
80
81fn handle_request(
82 req: &lsp_server::Request,
83 store: &Store,
84 config: &Config,
85 tag_index: &mut TagIndex,
86) -> Response {
87 match req.method.as_str() {
88 Formatting::METHOD if config.formatting => handlers::handle_formatting(req, store, config),
89 RangeFormatting::METHOD if config.formatting => {
90 handlers::handle_range_formatting(req, store, config)
91 }
92 CodeActionRequest::METHOD => handlers::handle_code_action(req, store, config, tag_index),
93 "workspace/symbol" => handlers::handle_workspace_symbol(req, tag_index),
94 DocumentSymbolRequest::METHOD => handlers::handle_document_symbol(req, store),
95 GotoDefinition::METHOD => handlers::handle_goto_definition(req, store, tag_index),
96 DocumentHighlightRequest::METHOD => handlers::handle_document_highlight(req, store),
97 FoldingRangeRequest::METHOD => handlers::handle_folding_range(req, store),
98 DocumentLinkRequest::METHOD => handlers::handle_document_link(req, store, tag_index),
99 Completion::METHOD => handlers::handle_completion(req, store, tag_index),
100 HoverRequest::METHOD if config.hover => handlers::handle_hover(req, store, tag_index),
101 References::METHOD => handlers::handle_references(req, store, tag_index),
102 Rename::METHOD => handlers::handle_rename(req, store, tag_index),
103 "textDocument/prepareRename" => handlers::handle_prepare_rename(req, store),
104 DocumentDiagnosticRequest::METHOD if config.diagnostics => {
105 handlers::handle_document_diagnostic(req, store, tag_index, config)
106 }
107 WorkspaceDiagnosticRequest::METHOD if config.diagnostics => {
108 handlers::handle_workspace_diagnostic(req, tag_index, config)
109 }
110 _ => Response {
111 id: req.id.clone(),
112 result: None,
113 error: Some(lsp_server::ResponseError {
114 code: lsp_server::ErrorCode::MethodNotFound as i32,
115 message: format!("unknown method: {}", req.method),
116 data: None,
117 }),
118 },
119 }
120}
121
122fn handle_notification(
123 notif: Notification,
124 store: &mut Store,
125 connection: &Connection,
126 config: &Config,
127 tag_index: &mut TagIndex,
128) -> Result<()> {
129 match notif.method.as_str() {
130 DidOpenTextDocument::METHOD => {
131 let params: lsp_types::DidOpenTextDocumentParams =
132 serde_json::from_value(notif.params)?;
133 let uri = params.text_document.uri;
134 let text = params.text_document.text;
135 store.open(uri.clone(), text);
136 if let Some((_text, doc)) = store.get(&uri) {
137 tag_index.update_file(&uri, doc);
138 }
139 if config.diagnostics && config.publish_diagnostics {
140 push_diagnostics(connection, &uri, store, tag_index, config)?;
141 }
142 }
143 DidChangeTextDocument::METHOD => {
144 let params: lsp_types::DidChangeTextDocumentParams =
145 serde_json::from_value(notif.params)?;
146 let uri = params.text_document.uri;
147 let text = params
148 .content_changes
149 .into_iter()
150 .last()
151 .ok_or_else(|| anyhow!("empty content changes"))?
152 .text;
153 store.change(&uri, text);
154 if let Some((_text, doc)) = store.get(&uri) {
155 tag_index.update_file(&uri, doc);
156 }
157 if config.diagnostics && config.publish_diagnostics {
158 push_diagnostics(connection, &uri, store, tag_index, config)?;
159 }
160 }
161 DidCloseTextDocument::METHOD => {
162 let params: lsp_types::DidCloseTextDocumentParams =
163 serde_json::from_value(notif.params)?;
164 store.close(¶ms.text_document.uri);
165 }
166 _ => {}
167 }
168 Ok(())
169}
170
171fn push_diagnostics(
172 connection: &Connection,
173 uri: &Uri,
174 store: &Store,
175 tag_index: &TagIndex,
176 config: &Config,
177) -> Result<()> {
178 let diags = store
179 .get(uri)
180 .map(|(_t, doc)| diagnostics::compute(doc, tag_index, uri, &config.diagnostic_levels))
181 .unwrap_or_default();
182
183 tracing::debug!(uri = %uri.as_str(), count = diags.len(), "publishing diagnostics");
184
185 let params = PublishDiagnosticsParams {
186 uri: uri.clone(),
187 diagnostics: diags,
188 version: None,
189 };
190 let notif = Notification {
191 method: PublishDiagnostics::METHOD.to_string(),
192 params: serde_json::to_value(params)?,
193 };
194 connection.sender.send(Message::Notification(notif))?;
195 Ok(())
196}
197
198pub(crate) fn make_response<T: serde::Serialize>(
199 req: &lsp_server::Request,
200 result: Result<T>,
201) -> Response {
202 match result.and_then(|val| serde_json::to_value(val).map_err(Into::into)) {
203 Ok(val) => Response {
204 id: req.id.clone(),
205 result: Some(val),
206 error: None,
207 },
208 Err(e) => Response {
209 id: req.id.clone(),
210 result: None,
211 error: Some(lsp_server::ResponseError {
212 code: lsp_server::ErrorCode::InternalError as i32,
213 message: e.to_string(),
214 data: None,
215 }),
216 },
217 }
218}
219
220pub fn uri_to_path(uri: &Uri) -> Option<PathBuf> {
221 let s = uri.as_str().strip_prefix("file://")?;
222 Some(PathBuf::from(percent_decode(s)))
223}
224
225fn percent_decode(s: &str) -> String {
226 let mut out = Vec::with_capacity(s.len());
227 let bytes = s.as_bytes();
228 let mut i = 0;
229 while i < bytes.len() {
230 if bytes[i] == b'%' && i + 2 < bytes.len() {
231 if let Some(b) = hex_pair(bytes[i + 1], bytes[i + 2]) {
232 out.push(b);
233 i += 3;
234 continue;
235 }
236 }
237 out.push(bytes[i]);
238 i += 1;
239 }
240 String::from_utf8(out).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
241}
242
243fn hex_pair(hi: u8, lo: u8) -> Option<u8> {
244 let h = match hi {
245 b'0'..=b'9' => hi - b'0',
246 b'A'..=b'F' => hi - b'A' + 10,
247 b'a'..=b'f' => hi - b'a' + 10,
248 _ => return None,
249 };
250 let l = match lo {
251 b'0'..=b'9' => lo - b'0',
252 b'A'..=b'F' => lo - b'A' + 10,
253 b'a'..=b'f' => lo - b'a' + 10,
254 _ => return None,
255 };
256 Some(h << 4 | l)
257}
258
259pub fn load_tag_path(tag_index: &mut TagIndex, path: &Path) {
260 if path.is_dir() {
261 let tags_file = path.join("tags");
262 if tags_file.exists() {
263 if let Err(e) = tag_index.load_tags_file(&tags_file) {
264 tracing::warn!(path = %tags_file.display(), error = %e, "failed to load tags file");
265 }
266 }
267 } else if path.exists() {
268 if let Err(e) = tag_index.load_tags_file(path) {
269 tracing::warn!(path = %path.display(), error = %e, "failed to load tags file");
270 }
271 }
272}
273
274pub(crate) fn text_end_position(text: &str) -> Position {
275 let mut line = 0u32;
276 let mut character = 0u32;
277 for ch in text.chars() {
278 if ch == '\n' {
279 line += 1;
280 character = 0;
281 } else {
282 #[allow(clippy::cast_possible_truncation)]
283 {
284 character += ch.len_utf16() as u32;
285 }
286 }
287 }
288 Position { line, character }
289}