Skip to main content

nifty_lsp/
server.rs

1//! The message loop: keeps open documents up to date and answers requests about them.
2
3use lsp_server::{Connection, ErrorCode, ExtractError, Message, Notification, Request, Response};
4use lsp_types::notification::{
5    DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, LogMessage,
6    Notification as LspNotification, PublishDiagnostics,
7};
8use lsp_types::request::{
9    Completion, DocumentHighlightRequest, DocumentSymbolRequest, GotoDefinition, HoverRequest,
10    References, Request as LspRequest, SemanticTokensFullRequest, SignatureHelpRequest,
11};
12use lsp_types::{
13    CompletionParams, CompletionResponse, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
14    DidOpenTextDocumentParams, DocumentHighlight, DocumentHighlightParams, DocumentSymbolParams,
15    DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams,
16    Location, LogMessageParams, MessageType, ReferenceParams, SemanticTokens, SemanticTokensParams,
17    SemanticTokensResult, SignatureHelp, SignatureHelpParams, TextDocumentPositionParams,
18};
19
20use crate::Result;
21use crate::document::Encoding;
22use crate::features::{completion, hover, navigation, outline, semantic_tokens, signature_help};
23use crate::project::Cursor;
24use crate::workspace::{Refreshed, Workspace};
25
26pub(crate) struct Server<'conn> {
27    connection: &'conn Connection,
28    encoding: Encoding,
29    workspace: Workspace,
30}
31
32impl<'conn> Server<'conn> {
33    pub(crate) fn new(connection: &'conn Connection, encoding: Encoding) -> Self {
34        Self {
35            connection,
36            encoding,
37            workspace: Workspace::default(),
38        }
39    }
40
41    /// Runs until the client asks the server to shut down or closes the connection.
42    pub(crate) fn run(mut self) -> Result<()> {
43        for message in &self.connection.receiver {
44            match message {
45                Message::Request(request) => {
46                    if self.connection.handle_shutdown(&request)? {
47                        return Ok(());
48                    }
49                    let response = self.handle_request(request);
50                    self.connection.sender.send(response.into())?;
51                }
52                Message::Notification(notification) => self.handle_notification(notification)?,
53                Message::Response(_) => {}
54            }
55        }
56        Ok(())
57    }
58
59    fn handle_request(&self, request: Request) -> Response {
60        RequestDispatch::new(self, request)
61            .on::<Completion>(Self::completion)
62            .on::<SignatureHelpRequest>(Self::signature_help)
63            .on::<HoverRequest>(Self::hover)
64            .on::<SemanticTokensFullRequest>(Self::semantic_tokens)
65            .on::<DocumentSymbolRequest>(Self::document_symbols)
66            .on::<GotoDefinition>(Self::definition)
67            .on::<References>(Self::references)
68            .on::<DocumentHighlightRequest>(Self::highlights)
69            .finish()
70    }
71
72    fn handle_notification(&mut self, notification: Notification) -> Result<()> {
73        let refreshed = match notification.method.as_str() {
74            DidOpenTextDocument::METHOD => {
75                let params: DidOpenTextDocumentParams = extract(notification)?;
76                let document = params.text_document;
77                self.workspace.update(
78                    &document.uri,
79                    document.text,
80                    document.version,
81                    self.encoding,
82                )
83            }
84            DidChangeTextDocument::METHOD => {
85                let mut params: DidChangeTextDocumentParams = extract(notification)?;
86                // The server asks for full sync, so the last change holds the whole text.
87                let Some(change) = params.content_changes.pop() else {
88                    return Ok(());
89                };
90                let document = params.text_document;
91                self.workspace
92                    .update(&document.uri, change.text, document.version, self.encoding)
93            }
94            DidCloseTextDocument::METHOD => {
95                let params: DidCloseTextDocumentParams = extract(notification)?;
96                self.workspace
97                    .close(&params.text_document.uri, self.encoding)
98            }
99            _ => return Ok(()),
100        };
101        self.send_refreshed(refreshed)
102    }
103
104    fn send_refreshed(&self, refreshed: Refreshed) -> Result<()> {
105        for message in refreshed.warnings {
106            let params = LogMessageParams {
107                typ: MessageType::WARNING,
108                message,
109            };
110            self.notify::<LogMessage>(params)?;
111        }
112        for params in refreshed.diagnostics {
113            self.notify::<PublishDiagnostics>(params)?;
114        }
115        Ok(())
116    }
117
118    fn notify<N: LspNotification>(&self, params: N::Params) -> Result<()> {
119        let notification = Notification::new(N::METHOD.to_owned(), params);
120        self.connection.sender.send(notification.into())?;
121        Ok(())
122    }
123
124    fn completion(&self, params: &CompletionParams) -> Option<CompletionResponse> {
125        let cursor = self.locate(&params.text_document_position)?;
126        completion::completions(cursor).map(CompletionResponse::Array)
127    }
128
129    fn signature_help(&self, params: &SignatureHelpParams) -> Option<SignatureHelp> {
130        signature_help::signature_help(self.locate(&params.text_document_position_params)?)
131    }
132
133    fn hover(&self, params: &HoverParams) -> Option<Hover> {
134        let cursor = self.locate(&params.text_document_position_params)?;
135        hover::hover(cursor, self.encoding)
136    }
137
138    fn semantic_tokens(&self, params: &SemanticTokensParams) -> Option<SemanticTokensResult> {
139        let (project, file) = self.workspace.file(&params.text_document.uri)?;
140        let data = semantic_tokens::semantic_tokens(project, file, self.encoding);
141        Some(SemanticTokensResult::Tokens(SemanticTokens {
142            result_id: None,
143            data,
144        }))
145    }
146
147    fn document_symbols(&self, params: &DocumentSymbolParams) -> Option<DocumentSymbolResponse> {
148        let (project, file) = self.workspace.file(&params.text_document.uri)?;
149        let symbols = outline::outline(project.document(file), self.encoding);
150        Some(DocumentSymbolResponse::Nested(symbols))
151    }
152
153    fn definition(&self, params: &GotoDefinitionParams) -> Option<GotoDefinitionResponse> {
154        let cursor = self.locate(&params.text_document_position_params)?;
155        let location = navigation::definition(cursor, self.encoding)?;
156        Some(GotoDefinitionResponse::Scalar(location))
157    }
158
159    fn references(&self, params: &ReferenceParams) -> Option<Vec<Location>> {
160        let cursor = self.locate(&params.text_document_position)?;
161        let include_declaration = params.context.include_declaration;
162        navigation::references(cursor, include_declaration, self.encoding)
163    }
164
165    fn highlights(&self, params: &DocumentHighlightParams) -> Option<Vec<DocumentHighlight>> {
166        let cursor = self.locate(&params.text_document_position_params)?;
167        navigation::highlights(cursor, self.encoding)
168    }
169
170    /// The project file a request points into, and the byte offset it points at.
171    fn locate(&self, params: &TextDocumentPositionParams) -> Option<Cursor<'_>> {
172        self.workspace
173            .locate(&params.text_document.uri, params.position, self.encoding)
174    }
175}
176
177/// Routes a request to the first handler registered for its method.
178struct RequestDispatch<'a, 'conn> {
179    server: &'a Server<'conn>,
180    state: DispatchState,
181}
182
183enum DispatchState {
184    Pending(Request),
185    Answered(Response),
186}
187
188impl<'a, 'conn> RequestDispatch<'a, 'conn> {
189    fn new(server: &'a Server<'conn>, request: Request) -> Self {
190        Self {
191            server,
192            state: DispatchState::Pending(request),
193        }
194    }
195
196    fn on<R: LspRequest>(self, handler: fn(&Server<'conn>, &R::Params) -> R::Result) -> Self {
197        let DispatchState::Pending(request) = self.state else {
198            return self;
199        };
200        let id = request.id.clone();
201        let state = match request.extract::<R::Params>(R::METHOD) {
202            Ok((id, params)) => {
203                DispatchState::Answered(Response::new_ok(id, handler(self.server, &params)))
204            }
205            Err(ExtractError::MethodMismatch(request)) => DispatchState::Pending(request),
206            Err(ExtractError::JsonError { method, error }) => {
207                DispatchState::Answered(Response::new_err(
208                    id,
209                    ErrorCode::InvalidParams as i32,
210                    format!("invalid params for {method}: {error}"),
211                ))
212            }
213        };
214        Self {
215            server: self.server,
216            state,
217        }
218    }
219
220    fn finish(self) -> Response {
221        match self.state {
222            DispatchState::Answered(response) => response,
223            DispatchState::Pending(request) => Response::new_err(
224                request.id,
225                ErrorCode::MethodNotFound as i32,
226                format!("unsupported request: {}", request.method),
227            ),
228        }
229    }
230}
231
232/// Malformed params for a notification the server relies on mean the client and server
233/// disagree on the protocol, so the error ends the session.
234fn extract<P: serde::de::DeserializeOwned>(notification: Notification) -> Result<P> {
235    let method = notification.method.clone();
236    notification
237        .extract(&method)
238        .map_err(|error| format!("invalid params for {method}: {error}").into())
239}