Skip to main content

vimdoc_language_server/
server.rs

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(&params.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}