1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | #![cfg (not(target_arch = "wasm32" ))] |
5 | |
6 | #[cfg (all(feature = "preview-engine" , not(feature = "preview-builtin" )))] |
7 | compile_error!("Feature preview-engine and preview-builtin need to be enabled together when building native LSP" ); |
8 | |
9 | mod common; |
10 | mod fmt; |
11 | mod language; |
12 | pub mod lsp_ext; |
13 | #[cfg (feature = "preview-engine" )] |
14 | mod preview; |
15 | pub mod util; |
16 | |
17 | use common::Result; |
18 | use language::*; |
19 | |
20 | use i_slint_compiler::CompilerConfiguration; |
21 | use lsp_types::notification::{ |
22 | DidChangeConfiguration, DidChangeTextDocument, DidOpenTextDocument, Notification, |
23 | }; |
24 | use lsp_types::{DidChangeTextDocumentParams, DidOpenTextDocumentParams, InitializeParams, Url}; |
25 | |
26 | use clap::{Args, Parser, Subcommand}; |
27 | use lsp_server::{Connection, ErrorCode, IoThreads, Message, RequestId, Response}; |
28 | use std::cell::RefCell; |
29 | use std::collections::HashMap; |
30 | use std::future::Future; |
31 | use std::pin::Pin; |
32 | use std::rc::Rc; |
33 | use std::sync::{atomic, Arc, Mutex}; |
34 | use std::task::{Poll, Waker}; |
35 | |
36 | #[derive (Clone, clap::Parser)] |
37 | #[command(author, version, about, long_about = None)] |
38 | pub struct Cli { |
39 | #[arg( |
40 | short = 'I' , |
41 | name = "Add include paths for the import statements" , |
42 | number_of_values = 1, |
43 | action |
44 | )] |
45 | include_paths: Vec<std::path::PathBuf>, |
46 | |
47 | /// The style name for the preview ('native' or 'fluent') |
48 | #[arg(long, name = "style name" , default_value_t, action)] |
49 | style: String, |
50 | |
51 | /// The backend or renderer used for the preview ('qt', 'femtovg', 'skia' or 'software') |
52 | #[arg(long, name = "backend" , default_value_t, action)] |
53 | backend: String, |
54 | |
55 | /// Start the preview in full screen mode |
56 | #[arg(long, action)] |
57 | fullscreen: bool, |
58 | |
59 | /// Hide the preview toolbar |
60 | #[arg(long, action)] |
61 | no_toolbar: bool, |
62 | |
63 | #[command(subcommand)] |
64 | command: Option<Commands>, |
65 | } |
66 | |
67 | #[derive (Subcommand, Clone)] |
68 | enum Commands { |
69 | /// Format slint files |
70 | Format(Format), |
71 | } |
72 | |
73 | #[derive (Args, Clone)] |
74 | struct Format { |
75 | #[arg(name = "path to .slint file(s)" , action)] |
76 | paths: Vec<std::path::PathBuf>, |
77 | |
78 | /// modify the file inline instead of printing to stdout |
79 | #[arg(short, long, action)] |
80 | inline: bool, |
81 | } |
82 | |
83 | enum OutgoingRequest { |
84 | Start, |
85 | Pending(Waker), |
86 | Done(lsp_server::Response), |
87 | } |
88 | |
89 | type OutgoingRequestQueue = Arc<Mutex<HashMap<RequestId, OutgoingRequest>>>; |
90 | |
91 | /// A handle that can be used to communicate with the client |
92 | /// |
93 | /// This type is duplicated, with the same interface, in wasm_main.rs |
94 | #[derive (Clone)] |
95 | pub struct ServerNotifier { |
96 | sender: crossbeam_channel::Sender<Message>, |
97 | queue: OutgoingRequestQueue, |
98 | use_external_preview: Arc<atomic::AtomicBool>, |
99 | #[cfg (feature = "preview-engine" )] |
100 | preview_to_lsp_sender: crossbeam_channel::Sender<crate::common::PreviewToLspMessage>, |
101 | } |
102 | |
103 | impl ServerNotifier { |
104 | pub fn use_external_preview(&self) -> bool { |
105 | self.use_external_preview.load(order:atomic::Ordering::Relaxed) |
106 | } |
107 | |
108 | pub fn set_use_external_preview(&self, is_external: bool) { |
109 | self.use_external_preview.store(val:is_external, order:atomic::Ordering::Release); |
110 | } |
111 | } |
112 | |
113 | impl ServerNotifier { |
114 | pub fn send_notification(&self, method: String, params: impl serde::Serialize) -> Result<()> { |
115 | self.sender.send(Message::Notification(lsp_server::Notification::new(method, params)))?; |
116 | Ok(()) |
117 | } |
118 | |
119 | pub fn send_request<T: lsp_types::request::Request>( |
120 | &self, |
121 | request: T::Params, |
122 | ) -> Result<impl Future<Output = Result<T::Result>>> { |
123 | static REQ_ID: atomic::AtomicI32 = atomic::AtomicI32::new(0); |
124 | let id = RequestId::from(REQ_ID.fetch_add(1, atomic::Ordering::Relaxed)); |
125 | let msg = |
126 | Message::Request(lsp_server::Request::new(id.clone(), T::METHOD.to_string(), request)); |
127 | self.sender.send(msg)?; |
128 | let queue = self.queue.clone(); |
129 | queue.lock().unwrap().insert(id.clone(), OutgoingRequest::Start); |
130 | Ok(std::future::poll_fn(move |ctx| { |
131 | let mut queue = queue.lock().unwrap(); |
132 | match queue.remove(&id).unwrap() { |
133 | OutgoingRequest::Pending(_) | OutgoingRequest::Start => { |
134 | queue.insert(id.clone(), OutgoingRequest::Pending(ctx.waker().clone())); |
135 | Poll::Pending |
136 | } |
137 | OutgoingRequest::Done(d) => { |
138 | if let Some(err) = d.error { |
139 | Poll::Ready(Err(err.message.into())) |
140 | } else { |
141 | Poll::Ready( |
142 | serde_json::from_value(d.result.unwrap_or_default()) |
143 | .map_err(|e| format!("cannot deserialize response: {e:?}" ).into()), |
144 | ) |
145 | } |
146 | } |
147 | } |
148 | })) |
149 | } |
150 | |
151 | pub fn send_message_to_preview(&self, message: common::LspToPreviewMessage) { |
152 | if self.use_external_preview() { |
153 | let _ = self.send_notification("slint/lsp_to_preview" .to_string(), message); |
154 | } else { |
155 | #[cfg (feature = "preview-builtin" )] |
156 | preview::lsp_to_preview_message(message, self); |
157 | } |
158 | } |
159 | |
160 | #[cfg (feature = "preview-engine" )] |
161 | pub fn send_message_to_lsp(&self, message: common::PreviewToLspMessage) { |
162 | let _ = self.preview_to_lsp_sender.send(message); |
163 | } |
164 | } |
165 | |
166 | impl RequestHandler { |
167 | async fn handle_request(&self, request: lsp_server::Request, ctx: &Rc<Context>) -> Result<()> { |
168 | if let Some(x: &Box) -> …>) = self.0.get(&request.method.as_str()) { |
169 | match x(request.params, ctx.clone()).await { |
170 | Ok(r: Value) => ctx |
171 | .server_notifier |
172 | .sender |
173 | .send(msg:Message::Response(Response::new_ok(request.id, result:r)))?, |
174 | Err(e: Box) => ctx.server_notifier.sender.send(msg:Message::Response(Response::new_err( |
175 | request.id, |
176 | code:ErrorCode::InternalError as i32, |
177 | message:e.to_string(), |
178 | )))?, |
179 | }; |
180 | } else { |
181 | ctx.server_notifier.sender.send(msg:Message::Response(Response::new_err( |
182 | request.id, |
183 | code:ErrorCode::MethodNotFound as i32, |
184 | message:"Cannot handle request" .into(), |
185 | )))?; |
186 | } |
187 | Ok(()) |
188 | } |
189 | } |
190 | |
191 | fn main() { |
192 | let args: Cli = Cli::parse(); |
193 | if !args.backend.is_empty() { |
194 | std::env::set_var("SLINT_BACKEND" , &args.backend); |
195 | } |
196 | |
197 | if let Some(Commands::Format(args)) = args.command { |
198 | let _ = fmt::tool::run(args.paths, args.inline).map_err(|e| { |
199 | eprintln!(" {e}" ); |
200 | std::process::exit(1); |
201 | }); |
202 | std::process::exit(0); |
203 | } |
204 | |
205 | #[cfg (feature = "preview-engine" )] |
206 | { |
207 | let cli_args = args.clone(); |
208 | let lsp_thread = std::thread::Builder::new() |
209 | .name("LanguageServer" .into()) |
210 | .spawn(move || { |
211 | /// Make sure we quit the event loop even if we panic |
212 | struct QuitEventLoop; |
213 | impl Drop for QuitEventLoop { |
214 | fn drop(&mut self) { |
215 | preview::quit_ui_event_loop(); |
216 | } |
217 | } |
218 | let quit_ui_loop = QuitEventLoop; |
219 | |
220 | let threads = match run_lsp_server(args) { |
221 | Ok(threads) => threads, |
222 | Err(error) => { |
223 | eprintln!("Error running LSP server: {}" , error); |
224 | return; |
225 | } |
226 | }; |
227 | |
228 | drop(quit_ui_loop); |
229 | threads.join().unwrap(); |
230 | }) |
231 | .unwrap(); |
232 | |
233 | preview::start_ui_event_loop(cli_args); |
234 | lsp_thread.join().unwrap(); |
235 | } |
236 | |
237 | #[cfg (not(feature = "preview-engine" ))] |
238 | match run_lsp_server(args) { |
239 | Ok(threads) => threads.join().unwrap(), |
240 | Err(error) => { |
241 | eprintln!("Error running LSP server: {}" , error); |
242 | } |
243 | } |
244 | } |
245 | |
246 | fn run_lsp_server(args: Cli) -> Result<IoThreads> { |
247 | let (connection: Connection, io_threads: IoThreads) = Connection::stdio(); |
248 | let (id: RequestId, params: Value) = connection.initialize_start()?; |
249 | |
250 | let init_param: InitializeParams = serde_json::from_value(params).unwrap(); |
251 | let initialize_result: Value = |
252 | serde_json::to_value(language::server_initialize_result(&init_param.capabilities))?; |
253 | connection.initialize_finish(initialize_id:id, initialize_result)?; |
254 | |
255 | main_loop(connection, init_param, cli_args:args)?; |
256 | |
257 | Ok(io_threads) |
258 | } |
259 | |
260 | fn main_loop(connection: Connection, init_param: InitializeParams, cli_args: Cli) -> Result<()> { |
261 | let mut rh = RequestHandler::default(); |
262 | register_request_handlers(&mut rh); |
263 | |
264 | let request_queue = OutgoingRequestQueue::default(); |
265 | #[cfg_attr (not(feature = "preview-engine" ), allow(unused))] |
266 | let (preview_to_lsp_sender, preview_to_lsp_receiver) = |
267 | crossbeam_channel::unbounded::<crate::common::PreviewToLspMessage>(); |
268 | |
269 | let server_notifier = ServerNotifier { |
270 | sender: connection.sender.clone(), |
271 | queue: request_queue.clone(), |
272 | use_external_preview: Default::default(), |
273 | #[cfg (feature = "preview-engine" )] |
274 | preview_to_lsp_sender, |
275 | }; |
276 | |
277 | let mut compiler_config = |
278 | CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Interpreter); |
279 | |
280 | compiler_config.style = |
281 | Some(if cli_args.style.is_empty() { "native" .into() } else { cli_args.style }); |
282 | compiler_config.include_paths = cli_args.include_paths; |
283 | let server_notifier_ = server_notifier.clone(); |
284 | compiler_config.open_import_fallback = Some(Rc::new(move |path| { |
285 | let server_notifier = server_notifier_.clone(); |
286 | Box::pin(async move { |
287 | let contents = std::fs::read_to_string(&path); |
288 | if let Ok(contents) = &contents { |
289 | if let Ok(url) = Url::from_file_path(&path) { |
290 | server_notifier.send_message_to_preview( |
291 | common::LspToPreviewMessage::SetContents { |
292 | url: common::VersionedUrl::new(url, None), |
293 | contents: contents.clone(), |
294 | }, |
295 | ) |
296 | } |
297 | } |
298 | Some(contents) |
299 | }) |
300 | })); |
301 | |
302 | let ctx = Rc::new(Context { |
303 | document_cache: RefCell::new(DocumentCache::new(compiler_config)), |
304 | server_notifier, |
305 | init_param, |
306 | to_show: Default::default(), |
307 | }); |
308 | |
309 | let mut futures = Vec::<Pin<Box<dyn Future<Output = Result<()>>>>>::new(); |
310 | let mut first_future = Box::pin(load_configuration(&ctx)); |
311 | |
312 | // We are waiting in this loop for two kind of futures: |
313 | // - The compiler future should always be ready immediately because we do not set a callback to load files |
314 | // - the future from `send_request` are blocked waiting for a response from the client. |
315 | // Responses are sent on the `connection.receiver` which will wake the loop, so there |
316 | // is no need to do anything in the Waker. |
317 | struct DummyWaker; |
318 | impl std::task::Wake for DummyWaker { |
319 | fn wake(self: Arc<Self>) {} |
320 | } |
321 | let waker = Arc::new(DummyWaker).into(); |
322 | match first_future.as_mut().poll(&mut std::task::Context::from_waker(&waker)) { |
323 | Poll::Ready(x) => x?, |
324 | Poll::Pending => futures.push(first_future), |
325 | }; |
326 | |
327 | loop { |
328 | crossbeam_channel::select! { |
329 | recv(connection.receiver) -> msg => { |
330 | match msg? { |
331 | Message::Request(req) => { |
332 | // ignore errors when shutdown |
333 | if connection.handle_shutdown(&req).unwrap_or(false) { |
334 | return Ok(()); |
335 | } |
336 | futures.push(Box::pin(rh.handle_request(req, &ctx))); |
337 | } |
338 | Message::Response(resp) => { |
339 | if let Some(q) = request_queue.lock().unwrap().get_mut(&resp.id) { |
340 | match q { |
341 | OutgoingRequest::Done(_) => { |
342 | return Err("Response to unknown request" .into()) |
343 | } |
344 | OutgoingRequest::Start => { /* nothing to do */ } |
345 | OutgoingRequest::Pending(x) => x.wake_by_ref(), |
346 | }; |
347 | *q = OutgoingRequest::Done(resp) |
348 | } else { |
349 | return Err("Response to unknown request" .into()); |
350 | } |
351 | } |
352 | Message::Notification(notification) => { |
353 | futures.push(Box::pin(handle_notification(notification, &ctx))) |
354 | } |
355 | } |
356 | }, |
357 | recv(preview_to_lsp_receiver) -> _msg => { |
358 | // Messages from the native preview come in here: |
359 | #[cfg (feature = "preview-engine" )] |
360 | futures.push(Box::pin(handle_preview_to_lsp_message(_msg?, &ctx))) |
361 | }, |
362 | }; |
363 | |
364 | let mut result = Ok(()); |
365 | futures.retain_mut(|f| { |
366 | if result.is_err() { |
367 | return true; |
368 | } |
369 | match f.as_mut().poll(&mut std::task::Context::from_waker(&waker)) { |
370 | Poll::Ready(x) => { |
371 | result = x; |
372 | false |
373 | } |
374 | Poll::Pending => true, |
375 | } |
376 | }); |
377 | result?; |
378 | } |
379 | } |
380 | |
381 | async fn handle_notification(req: lsp_server::Notification, ctx: &Rc<Context>) -> Result<()> { |
382 | match &*req.method { |
383 | DidOpenTextDocument::METHOD => { |
384 | let params: DidOpenTextDocumentParams = serde_json::from_value(req.params)?; |
385 | reload_document( |
386 | ctx, |
387 | params.text_document.text, |
388 | params.text_document.uri, |
389 | Some(params.text_document.version), |
390 | &mut ctx.document_cache.borrow_mut(), |
391 | ) |
392 | .await |
393 | } |
394 | DidChangeTextDocument::METHOD => { |
395 | let mut params: DidChangeTextDocumentParams = serde_json::from_value(req.params)?; |
396 | reload_document( |
397 | ctx, |
398 | params.content_changes.pop().unwrap().text, |
399 | params.text_document.uri, |
400 | Some(params.text_document.version), |
401 | &mut ctx.document_cache.borrow_mut(), |
402 | ) |
403 | .await |
404 | } |
405 | DidChangeConfiguration::METHOD => load_configuration(ctx).await, |
406 | |
407 | #[cfg (any(feature = "preview-builtin" , feature = "preview-external" ))] |
408 | "slint/showPreview" => { |
409 | language::show_preview_command(req.params.as_array().map_or(&[], |x| x.as_slice()), ctx) |
410 | } |
411 | |
412 | // Messages from the WASM preview come in as notifications sent by the "editor": |
413 | #[cfg (any(feature = "preview-external" , feature = "preview-engine" ))] |
414 | "slint/preview_to_lsp" => { |
415 | handle_preview_to_lsp_message(serde_json::from_value(req.params)?, ctx).await |
416 | } |
417 | _ => Ok(()), |
418 | } |
419 | } |
420 | |
421 | #[cfg (any(feature = "preview-external" , feature = "preview-engine" ))] |
422 | async fn send_workspace_edit( |
423 | server_notifier: ServerNotifier, |
424 | label: Option<String>, |
425 | edit: Result<lsp_types::WorkspaceEdit>, |
426 | ) -> Result<()> { |
427 | let edit: WorkspaceEdit = edit?; |
428 | |
429 | let response: ApplyWorkspaceEditResponse = server_notifierimpl Future |
430 | .send_request::<lsp_types::request::ApplyWorkspaceEdit>( |
431 | lsp_types::ApplyWorkspaceEditParams { label, edit }, |
432 | )? |
433 | .await?; |
434 | if !response.applied { |
435 | return Err(responseString |
436 | .failure_reason |
437 | .unwrap_or(default:"Operation failed, no specific reason given" .into()) |
438 | .into()); |
439 | } |
440 | Ok(()) |
441 | } |
442 | |
443 | #[cfg (any(feature = "preview-external" , feature = "preview-engine" ))] |
444 | async fn handle_preview_to_lsp_message( |
445 | message: crate::common::PreviewToLspMessage, |
446 | ctx: &Rc<Context>, |
447 | ) -> Result<()> { |
448 | use crate::common::PreviewToLspMessage as M; |
449 | match message { |
450 | M::Status { message, health } => { |
451 | crate::common::lsp_to_editor::send_status_notification( |
452 | &ctx.server_notifier, |
453 | &message, |
454 | health, |
455 | ); |
456 | } |
457 | M::Diagnostics { uri, diagnostics } => { |
458 | crate::common::lsp_to_editor::notify_lsp_diagnostics( |
459 | &ctx.server_notifier, |
460 | uri, |
461 | diagnostics, |
462 | ); |
463 | } |
464 | M::ShowDocument { file, selection } => { |
465 | crate::common::lsp_to_editor::send_show_document_to_editor( |
466 | ctx.server_notifier.clone(), |
467 | file, |
468 | selection, |
469 | ) |
470 | .await; |
471 | } |
472 | M::PreviewTypeChanged { is_external } => { |
473 | ctx.server_notifier.set_use_external_preview(is_external); |
474 | } |
475 | M::RequestState { .. } => { |
476 | crate::language::request_state(ctx); |
477 | } |
478 | M::UpdateElement { label, position, properties } => { |
479 | let _ = send_workspace_edit( |
480 | ctx.server_notifier.clone(), |
481 | label, |
482 | properties::update_element_properties(ctx, position, properties), |
483 | ) |
484 | .await; |
485 | } |
486 | M::SendWorkspaceEdit { label, edit } => { |
487 | let _ = send_workspace_edit(ctx.server_notifier.clone(), label, Ok(edit)).await; |
488 | } |
489 | } |
490 | Ok(()) |
491 | } |
492 | |