git-ents.gitmain
⌘K
foforge
server.rs353 lines · 14.6 KB · rusthistorycomment on this file
1//! The stdio Language Server Protocol adapter (`lens.serve`): a thin,
2//! synchronous dispatch loop over `lsp-server` that forwards each request to
3//! a [`Lens`] method and turns the [`Outcome`] back into LSP messages.
4//!
5//! All derivation lives in [`Lens`]; this module only frames JSON-RPC,
6//! declares capabilities, and routes. It binds no socket and adds no git
7//! transport (`lens.serve`) — the only IO is stdin/stdout.
8//!
9//! `lsp-server` (rust-analyzer's own scaffold) is synchronous, which suits
10//! the lens exactly: every operation it performs — reading `refs/meta/*`,
11//! diffing against the working tree, writing a signed commit — is blocking
12//! git and filesystem work, so an async runtime would only wrap blocking
13//! calls in `spawn_blocking` for no gain. The framing and pack of message
14//! types come from the crate; nothing here hand-rolls JSON-RPC.
15#![expect(
16 clippy::let_underscore_must_use,
17 reason = "sending on the LSP transport is best-effort: a closed pipe ends the loop on the \
18 next receive, so a failed send needs no separate handling"
19)]
20
21use gix_object::{Find, Write};
22use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response};
23use lsp_types::notification::{
24 DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, DidSaveTextDocument,
25 Notification as _, PublishDiagnostics,
26};
27use lsp_types::request::{
28 CodeActionRequest, CodeLensRequest, ExecuteCommand, HoverRequest, Request as _, ShowDocument,
29};
30use lsp_types::{
31 CodeActionProviderCapability, CodeLensOptions, DidChangeTextDocumentParams,
32 DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams,
33 ExecuteCommandOptions, HoverProviderCapability, PublishDiagnosticsParams, ServerCapabilities,
34 ShowDocumentParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
35 TextDocumentSyncSaveOptions, Url,
36};
37
38use crate::lens::{Lens, Outcome};
39use crate::render;
40
41/// The capabilities the lens advertises (`lens.serve`): full-text document
42/// sync with save notifications (the compose flow needs the save,
43/// `lens.compose`), code lenses (`lens.lenses`), hover (`lens.hover`), code
44/// actions (`lens.compose`), and the four executable commands
45/// (`lens.lenses`, `lens.compose`). No workspace, symbol, or completion
46/// surface — the lens is a conversation view, not a language analyzer.
47#[must_use]
48pub fn capabilities() -> ServerCapabilities {
49 ServerCapabilities {
50 text_document_sync: Some(TextDocumentSyncCapability::Options(
51 TextDocumentSyncOptions {
52 open_close: Some(true),
53 change: Some(TextDocumentSyncKind::FULL),
54 save: Some(TextDocumentSyncSaveOptions::Supported(true)),
55 ..TextDocumentSyncOptions::default()
56 },
57 )),
58 code_lens_provider: Some(CodeLensOptions {
59 resolve_provider: Some(false),
60 }),
61 hover_provider: Some(HoverProviderCapability::Simple(true)),
62 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
63 execute_command_provider: Some(ExecuteCommandOptions {
64 commands: vec![
65 render::CMD_VIEW.to_owned(),
66 render::CMD_REPLY.to_owned(),
67 render::CMD_RESOLVE.to_owned(),
68 render::CMD_COMPOSE.to_owned(),
69 ],
70 work_done_progress_options: lsp_types::WorkDoneProgressOptions::default(),
71 }),
72 ..ServerCapabilities::default()
73 }
74}
75
76/// Serve the lens over stdio until the client shuts it down (`lens.serve`).
77///
78/// Performs the LSP initialize handshake advertising [`capabilities`], then
79/// runs the dispatch loop. Binds no socket and speaks only stdin/stdout.
80///
81/// # Errors
82///
83/// [`std::io::Error`] if the JSON-RPC transport fails (a broken pipe, a
84/// malformed frame) or the initialize handshake does not complete.
85pub fn serve_stdio<O: Find + Write>(lens: Lens<O>) -> std::io::Result<()> {
86 let (connection, io_threads) = Connection::stdio();
87 let capabilities = serde_json::to_value(capabilities())
88 .map_err(|source| std::io::Error::other(source.to_string()))?;
89 let _init_params = connection
90 .initialize(capabilities)
91 .map_err(|source| std::io::Error::other(source.to_string()))?;
92 let mut server = ServerLoop { connection, lens };
93 server.run()?;
94 // Drop the connection (and with it the writer's channel sender) before
95 // joining: the IO writer thread only terminates once its sender is gone,
96 // so joining while `server` still holds the connection would hang here
97 // forever.
98 drop(server);
99 io_threads.join()?;
100 Ok(())
101}
102
103/// The running dispatch loop: owns the connection and the lens, and a
104/// counter for the ids of the server-initiated requests it sends (only
105/// `window/showDocument`).
106struct ServerLoop<O> {
107 connection: Connection,
108 lens: Lens<O>,
109}
110
111impl<O: Find + Write> ServerLoop<O> {
112 fn run(&mut self) -> std::io::Result<()> {
113 // `iter()` yields until the client closes the connection; a
114 // `shutdown` request breaks the loop through `handle_shutdown`.
115 while let Ok(message) = self.connection.receiver.recv() {
116 match message {
117 Message::Request(request) => {
118 if self
119 .connection
120 .handle_shutdown(&request)
121 .map_err(|source| std::io::Error::other(source.to_string()))?
122 {
123 break;
124 }
125 self.on_request(request);
126 }
127 Message::Notification(notification) => self.on_notification(notification),
128 // Responses to our own `window/showDocument` requests carry
129 // nothing the lens needs to act on.
130 Message::Response(_) => {}
131 }
132 }
133 Ok(())
134 }
135
136 /// Route one request to a [`Lens`] read handler, replying with its
137 /// result or an error response.
138 fn on_request(&mut self, request: Request) {
139 let request = match self.request::<CodeLensRequest, _>(request, |lens, params| {
140 lens.code_lenses(&params.text_document.uri).map(Some)
141 }) {
142 Ok(()) => return,
143 Err(request) => request,
144 };
145 let request = match self.request::<HoverRequest, _>(request, |lens, params| {
146 let position = params.text_document_position_params;
147 lens.hover(&position.text_document.uri, position.position)
148 }) {
149 Ok(()) => return,
150 Err(request) => request,
151 };
152 let request = match self.request::<CodeActionRequest, _>(request, |lens, params| {
153 lens.code_actions(&params.text_document.uri, params.range)
154 .map(Some)
155 }) {
156 Ok(()) => return,
157 Err(request) => request,
158 };
159 // `executeCommand` is the one request with side effects, handled on
160 // its own so its `Outcome` can drive `showDocument` and refreshes.
161 let request = match self.on_execute_command(request) {
162 Ok(()) => return,
163 Err(request) => request,
164 };
165 // Any other request: an empty success, so a client probing an
166 // unsupported method gets a well-formed (null) reply, never a hang.
167 self.respond(Response::new_ok(request.id, serde_json::Value::Null));
168 }
169
170 /// Extract and answer a plain read request `R`, returning `Err(request)`
171 /// unchanged when it is a different method so the caller can try the
172 /// next.
173 fn request<R, F>(&mut self, request: Request, handle: F) -> Result<(), Request>
174 where
175 R: lsp_types::request::Request,
176 F: FnOnce(&Lens<O>, R::Params) -> crate::error::Result<R::Result>,
177 R::Result: serde::Serialize,
178 {
179 match request.extract::<R::Params>(R::METHOD) {
180 Ok((id, params)) => {
181 let response = match handle(&self.lens, params) {
182 Ok(result) => Response::new_ok(id, result),
183 Err(error) => error_response(id, &error),
184 };
185 self.respond(response);
186 Ok(())
187 }
188 Err(ExtractError::MethodMismatch(request)) => Err(request),
189 Err(ExtractError::JsonError { .. }) => {
190 // A malformed params payload for a method we do own: there is
191 // no id to reply against cleanly here, so drop it — the
192 // client will observe the missing response.
193 Ok(())
194 }
195 }
196 }
197
198 /// Handle `workspace/executeCommand`, applying the [`Outcome`]: reply
199 /// with its result value, open the compose template if it named one, and
200 /// republish diagnostics when a mutation invalidated them.
201 fn on_execute_command(&mut self, request: Request) -> Result<(), Request> {
202 match request.extract::<<ExecuteCommand as lsp_types::request::Request>::Params>(
203 ExecuteCommand::METHOD,
204 ) {
205 Ok((id, params)) => {
206 match self
207 .lens
208 .execute_command(&params.command, &params.arguments)
209 {
210 Ok(outcome) => {
211 let value = outcome.response.clone().unwrap_or(serde_json::Value::Null);
212 self.respond(Response::new_ok(id, value));
213 self.apply(outcome);
214 }
215 Err(error) => self.respond(error_response(id, &error)),
216 }
217 Ok(())
218 }
219 Err(ExtractError::MethodMismatch(request)) => Err(request),
220 Err(ExtractError::JsonError { .. }) => Ok(()),
221 }
222 }
223
224 /// Route one notification to the matching [`Lens`] document or save
225 /// handler, republishing diagnostics as the sync events demand.
226 fn on_notification(&mut self, notification: lsp_server::Notification) {
227 match notification.method.as_str() {
228 DidOpenTextDocument::METHOD => {
229 if let Ok(params) = extract_notification::<DidOpenTextDocumentParams>(notification)
230 {
231 let uri = params.text_document.uri.clone();
232 self.lens.did_open(uri.clone(), params.text_document.text);
233 self.publish(&uri);
234 }
235 }
236 DidChangeTextDocument::METHOD => {
237 if let Ok(params) =
238 extract_notification::<DidChangeTextDocumentParams>(notification)
239 {
240 let uri = params.text_document.uri.clone();
241 // Full sync: the last change carries the whole buffer.
242 if let Some(change) = params.content_changes.into_iter().next_back() {
243 self.lens.did_change(uri.clone(), change.text);
244 }
245 self.publish(&uri);
246 }
247 }
248 DidCloseTextDocument::METHOD => {
249 if let Ok(params) = extract_notification::<DidCloseTextDocumentParams>(notification)
250 {
251 self.lens.did_close(&params.text_document.uri);
252 // Clear this document's diagnostics on close.
253 self.publish_list(&params.text_document.uri, Vec::new());
254 }
255 }
256 DidSaveTextDocument::METHOD => {
257 if let Ok(params) = extract_notification::<DidSaveTextDocumentParams>(notification)
258 {
259 match self.lens.did_save(&params.text_document.uri) {
260 Ok(outcome) => self.apply(outcome),
261 Err(error) => log(&format!("didSave: {error}")),
262 }
263 }
264 }
265 _ => {}
266 }
267 }
268
269 /// Apply an [`Outcome`]'s side effects: open the compose template and/or
270 /// republish diagnostics for every open document.
271 fn apply(&mut self, outcome: Outcome) {
272 if let Some(path) = outcome.show_document
273 && let Some(uri) = crate::document::file_uri(&path)
274 {
275 self.show_document(uri);
276 }
277 if outcome.refresh {
278 for uri in self.lens.open_documents() {
279 self.publish(&uri);
280 }
281 }
282 }
283
284 /// Compute and publish diagnostics for one document (`lens.diagnostics`).
285 fn publish(&self, uri: &Url) {
286 match self.lens.diagnostics_for(uri) {
287 Ok(diagnostics) => self.publish_list(uri, diagnostics),
288 Err(error) => log(&format!("diagnostics for {uri}: {error}")),
289 }
290 }
291
292 /// Send a `textDocument/publishDiagnostics` notification.
293 fn publish_list(&self, uri: &Url, diagnostics: Vec<lsp_types::Diagnostic>) {
294 let params = PublishDiagnosticsParams {
295 uri: uri.clone(),
296 diagnostics,
297 version: None,
298 };
299 let notification =
300 lsp_server::Notification::new(PublishDiagnostics::METHOD.to_owned(), params);
301 let _ = self
302 .connection
303 .sender
304 .send(Message::Notification(notification));
305 }
306
307 /// Ask the client to open `uri` (`window/showDocument`) — the compose
308 /// template (`lens.compose`).
309 fn show_document(&self, uri: Url) {
310 let params = ShowDocumentParams {
311 uri,
312 external: Some(false),
313 take_focus: Some(true),
314 selection: None,
315 };
316 let request = Request {
317 // A fixed id: the lens never correlates showDocument responses,
318 // and only one is ever in flight per user action.
319 id: RequestId::from("ents-show-document".to_owned()),
320 method: ShowDocument::METHOD.to_owned(),
321 params: serde_json::to_value(params).unwrap_or(serde_json::Value::Null),
322 };
323 let _ = self.connection.sender.send(Message::Request(request));
324 }
325
326 fn respond(&self, response: Response) {
327 let _ = self.connection.sender.send(Message::Response(response));
328 }
329}
330
331/// Build an LSP error response from a lens error, so a failing request gets
332/// a well-formed fault rather than a dropped reply.
333fn error_response(id: RequestId, error: &crate::error::Error) -> Response {
334 Response::new_err(
335 id,
336 lsp_server::ErrorCode::RequestFailed as i32,
337 error.to_string(),
338 )
339}
340
341/// Deserialize a notification's params, returning `Err` on a mismatch or a
342/// malformed payload.
343fn extract_notification<P: serde::de::DeserializeOwned>(
344 notification: lsp_server::Notification,
345) -> Result<P, ()> {
346 serde_json::from_value(notification.params).map_err(|_error| ())
347}
348
349/// Emit a diagnostic line on stderr — the lens's own log channel, since
350/// stdout carries the LSP framing.
351fn log(message: &str) {
352 eprintln!("ents-lens: {message}");
353}