Fork of daniellemaywood.uk/gleam — Wasm codegen work
12 kB
294 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2024 The Gleam contributors
3
4use camino::Utf8PathBuf;
5use lsp::{DefinitionRequest, DidChangeWatchedFilesNotification, DidOpenTextDocumentNotification};
6use lsp_types::{
7 self as lsp, CodeActionRequest, CompletionRequest, DidChangeTextDocumentNotification,
8 DidCloseTextDocumentNotification, DidSaveTextDocumentNotification, DocumentFormattingRequest,
9 DocumentHighlightRequest, DocumentSymbolRequest, FoldingRangeRequest, HoverRequest,
10 PrepareRenameRequest, ReferencesRequest, RenameRequest, SignatureHelpRequest,
11 TextDocumentContentChangeEvent, TypeDefinitionRequest, WillRenameFilesRequest,
12};
13use std::time::Duration;
14
15#[derive(Debug)]
16pub enum Message {
17 Request(lsp_server::RequestId, Request),
18 Notification(Notification),
19}
20
21#[derive(Debug)]
22pub enum Request {
23 Format(lsp::DocumentFormattingParams),
24 Hover(lsp::HoverParams),
25 GoToDefinition(lsp::DefinitionParams),
26 GoToTypeDefinition(lsp::TypeDefinitionParams),
27 Completion(lsp::CompletionParams),
28 CodeAction(lsp::CodeActionParams),
29 SignatureHelp(lsp::SignatureHelpParams),
30 DocumentSymbol(lsp::DocumentSymbolParams),
31 FoldingRange(lsp::FoldingRangeParams),
32 PrepareRename(lsp::PrepareRenameParams),
33 Rename(lsp::RenameParams),
34 FindReferences(lsp::ReferenceParams),
35 DocumentHighlight(lsp::DocumentHighlightParams),
36 RenameFiles(lsp::RenameFilesParams),
37}
38
39impl Request {
40 fn extract(request: lsp_server::Request) -> Option<Message> {
41 let id = request.id.clone();
42 match request.method.as_str() {
43 "textDocument/formatting" => {
44 let params = cast_request::<DocumentFormattingRequest>(request);
45 Some(Message::Request(id, Request::Format(params)))
46 }
47 "textDocument/hover" => {
48 let params = cast_request::<HoverRequest>(request);
49 Some(Message::Request(id, Request::Hover(params)))
50 }
51 "textDocument/definition" => {
52 let params = cast_request::<DefinitionRequest>(request);
53 Some(Message::Request(id, Request::GoToDefinition(params)))
54 }
55 "textDocument/completion" => {
56 let params = cast_request::<CompletionRequest>(request);
57 Some(Message::Request(id, Request::Completion(params)))
58 }
59 "textDocument/codeAction" => {
60 let params = cast_request::<CodeActionRequest>(request);
61 Some(Message::Request(id, Request::CodeAction(params)))
62 }
63 "textDocument/signatureHelp" => {
64 let params = cast_request::<SignatureHelpRequest>(request);
65 Some(Message::Request(id, Request::SignatureHelp(params)))
66 }
67 "textDocument/documentSymbol" => {
68 let params = cast_request::<DocumentSymbolRequest>(request);
69 Some(Message::Request(id, Request::DocumentSymbol(params)))
70 }
71 "textDocument/foldingRange" => {
72 let params = cast_request::<FoldingRangeRequest>(request);
73 Some(Message::Request(id, Request::FoldingRange(params)))
74 }
75 "textDocument/rename" => {
76 let params = cast_request::<RenameRequest>(request);
77 Some(Message::Request(id, Request::Rename(params)))
78 }
79 "textDocument/prepareRename" => {
80 let params = cast_request::<PrepareRenameRequest>(request);
81 Some(Message::Request(id, Request::PrepareRename(params)))
82 }
83 "textDocument/typeDefinition" => {
84 let params = cast_request::<TypeDefinitionRequest>(request);
85 Some(Message::Request(id, Request::GoToTypeDefinition(params)))
86 }
87 "textDocument/references" => {
88 let params = cast_request::<ReferencesRequest>(request);
89 Some(Message::Request(id, Request::FindReferences(params)))
90 }
91 "textDocument/documentHighlight" => {
92 let params = cast_request::<DocumentHighlightRequest>(request);
93 Some(Message::Request(id, Request::DocumentHighlight(params)))
94 }
95 "workspace/willRenameFiles" => {
96 let params = cast_request::<WillRenameFilesRequest>(request);
97 Some(Message::Request(id, Request::RenameFiles(params)))
98 }
99 _ => None,
100 }
101 }
102}
103
104#[derive(Debug)]
105pub enum Notification {
106 /// A Gleam file has been modified in memory, and the new text is provided.
107 SourceFileChangedInMemory { path: Utf8PathBuf, text: String },
108 /// A Gleam file has been opened in the editor.
109 SourceFileOpened { path: Utf8PathBuf, text: String },
110 /// A Gleam file has been closed in the editor.
111 SourceFileClosed { path: Utf8PathBuf },
112 /// A gleam file has been saved to disk
113 SourceFileSaved { path: Utf8PathBuf },
114 /// gleam.toml has changed.
115 ConfigFileChanged { path: Utf8PathBuf },
116 /// It's time to compile all open projects.
117 CompilePlease,
118}
119
120impl Notification {
121 fn extract(notification: lsp_server::Notification) -> Option<Message> {
122 match notification.method.as_str() {
123 "textDocument/didOpen" => {
124 let params = cast_notification::<DidOpenTextDocumentNotification>(notification);
125 let notification = Notification::SourceFileOpened {
126 path: super::path(¶ms.text_document.uri),
127 text: params.text_document.text,
128 };
129 Some(Message::Notification(notification))
130 }
131 "textDocument/didChange" => {
132 let params = cast_notification::<DidChangeTextDocumentNotification>(notification);
133 let TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(change) =
134 params.content_changes.into_iter().next_back()?
135 else {
136 return None;
137 };
138 let notification = Notification::SourceFileChangedInMemory {
139 path: super::path(¶ms.text_document.text_document_identifier.uri),
140 text: change.text,
141 };
142 Some(Message::Notification(notification))
143 }
144 "textDocument/didSave" => {
145 let params = cast_notification::<DidSaveTextDocumentNotification>(notification);
146 let notification = Notification::SourceFileSaved {
147 path: super::path(¶ms.text_document.uri),
148 };
149 Some(Message::Notification(notification))
150 }
151 "textDocument/didClose" => {
152 let params = cast_notification::<DidCloseTextDocumentNotification>(notification);
153 let notification = Notification::SourceFileClosed {
154 path: super::path(¶ms.text_document.uri),
155 };
156 Some(Message::Notification(notification))
157 }
158 "workspace/didChangeWatchedFiles" => {
159 let params = cast_notification::<DidChangeWatchedFilesNotification>(notification);
160 let notification = Notification::ConfigFileChanged {
161 path: super::path(¶ms.changes.into_iter().next_back()?.uri),
162 };
163 Some(Message::Notification(notification))
164 }
165 _ => None,
166 }
167 }
168}
169
170pub enum Next {
171 MorePlease,
172 Handle(Vec<Message>),
173 Stop,
174}
175
176/// The message buffer pulls messages from the client until one of the following
177/// happens:
178/// - A shutdown request is received.
179/// - A short pause in messages is detected, indicating the programmer has
180/// stopped typing for a moment and would benefit from feedback.
181/// - A request type message is received, which requires an immediate response.
182///
183pub struct MessageBuffer {
184 messages: Vec<Message>,
185}
186
187impl MessageBuffer {
188 pub fn new() -> Self {
189 Self {
190 messages: Vec::new(),
191 }
192 }
193
194 pub fn receive(&mut self, conn: &lsp_server::Connection) -> Next {
195 let pause = Duration::from_millis(100);
196
197 // If the buffer is empty, wait indefinitely for the first message.
198 // If the buffer is not empty, wait for a short time to see if more messages are
199 // coming before processing the ones we have.
200 let message = if self.messages.is_empty() {
201 Some(conn.receiver.recv().expect("Receiving LSP message"))
202 } else {
203 conn.receiver.recv_timeout(pause).ok()
204 };
205
206 // If have have not received a message then it means there is a pause in the
207 // messages from the client, implying the programmer has stopped typing. Process
208 // the currently enqueued messages.
209 let message = match message {
210 Some(message) => message,
211 None => {
212 // A compile please message it added in the instance of this
213 // pause of activity so that the client gets feedback on the
214 // state of the code as it is now.
215 self.push_compile_please_message();
216 return Next::Handle(self.take_messages());
217 }
218 };
219
220 match message {
221 lsp_server::Message::Request(r) if self.shutdown(conn, &r) => Next::Stop,
222 lsp_server::Message::Request(r) => self.request(r),
223 lsp_server::Message::Response(r) => self.response(r),
224 lsp_server::Message::Notification(n) => self.notification(n),
225 }
226 }
227
228 fn request(&mut self, r: lsp_server::Request) -> Next {
229 let Some(message) = Request::extract(r) else {
230 return Next::MorePlease;
231 };
232
233 // Compile the code prior to attempting to process the response, to
234 // ensure that the response is based on the latest code.
235 self.push_compile_please_message();
236 self.messages.push(message);
237 Next::Handle(self.take_messages())
238 }
239
240 fn notification(&mut self, n: lsp_server::Notification) -> Next {
241 // A new notification telling us that an edit has been made, or
242 // something along those lines.
243 if let Some(message) = Notification::extract(n) {
244 self.messages.push(message);
245 }
246 // Ask for more messages (or a pause), at which point we'll start processing.
247 Next::MorePlease
248 }
249
250 fn response(&mut self, _: lsp_server::Response) -> Next {
251 // We do not use or expect responses from the client currently.
252 Next::MorePlease
253 }
254
255 /// Add a `CompilePlease` message which will prompt the engine to compile
256 /// the projects.
257 ///
258 fn push_compile_please_message(&mut self) {
259 let message = Notification::CompilePlease;
260 let value = Message::Notification(message);
261 self.messages.push(value);
262 }
263
264 fn take_messages(&mut self) -> Vec<Message> {
265 std::mem::take(&mut self.messages)
266 }
267
268 fn shutdown(
269 &mut self,
270 connection: &lsp_server::Connection,
271 request: &lsp_server::Request,
272 ) -> bool {
273 connection.handle_shutdown(request).expect("LSP shutdown")
274 }
275}
276
277fn cast_request<R>(request: lsp_server::Request) -> R::Params
278where
279 R: lsp::Request,
280 R::Params: serde::de::DeserializeOwned,
281{
282 let (_, params) = request.extract(R::METHOD.as_str()).expect("cast request");
283 params
284}
285
286fn cast_notification<N>(notification: lsp_server::Notification) -> N::Params
287where
288 N: lsp::Notification,
289 N::Params: serde::de::DeserializeOwned,
290{
291 notification
292 .extract::<N::Params>(N::METHOD.as_str())
293 .expect("cast notification")
294}