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")))]
7compile_error!("Feature preview-engine and preview-builtin need to be enabled together when building native LSP");
8
9mod common;
10mod fmt;
11mod language;
12pub mod lsp_ext;
13#[cfg(feature = "preview-engine")]
14mod preview;
15pub mod util;
16
17use common::Result;
18use language::*;
19
20use i_slint_compiler::CompilerConfiguration;
21use lsp_types::notification::{
22 DidChangeConfiguration, DidChangeTextDocument, DidOpenTextDocument, Notification,
23};
24use lsp_types::{DidChangeTextDocumentParams, DidOpenTextDocumentParams, InitializeParams, Url};
25
26use clap::{Args, Parser, Subcommand};
27use lsp_server::{Connection, ErrorCode, IoThreads, Message, RequestId, Response};
28use std::cell::RefCell;
29use std::collections::HashMap;
30use std::future::Future;
31use std::pin::Pin;
32use std::rc::Rc;
33use std::sync::{atomic, Arc, Mutex};
34use std::task::{Poll, Waker};
35
36#[derive(Clone, clap::Parser)]
37#[command(author, version, about, long_about = None)]
38pub 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)]
68enum Commands {
69 /// Format slint files
70 Format(Format),
71}
72
73#[derive(Args, Clone)]
74struct 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
83enum OutgoingRequest {
84 Start,
85 Pending(Waker),
86 Done(lsp_server::Response),
87}
88
89type 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)]
95pub 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
103impl 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
113impl 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
166impl 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
191fn 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
246fn 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
260fn 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
381async 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"))]
422async 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"))]
444async 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