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(target_arch = "wasm32")]
5
6pub mod common;
7mod fmt;
8mod language;
9pub mod lsp_ext;
10#[cfg(feature = "preview-engine")]
11mod preview;
12pub mod util;
13
14use common::{LspToPreviewMessage, Result, VersionedUrl};
15use i_slint_compiler::CompilerConfiguration;
16use js_sys::Function;
17pub use language::{Context, DocumentCache, RequestHandler};
18use lsp_types::Url;
19use serde::Serialize;
20use std::cell::RefCell;
21use std::future::Future;
22use std::io::ErrorKind;
23use std::rc::Rc;
24use wasm_bindgen::prelude::*;
25
26#[cfg(target_arch = "wasm32")]
27use crate::wasm_prelude::*;
28
29type JsResult<T> = std::result::Result<T, JsError>;
30
31pub mod wasm_prelude {
32 use std::path::{Path, PathBuf};
33
34 /// lsp_url doesn't have method to convert to and from PathBuf for wasm, so just make some
35 pub trait UrlWasm {
36 fn to_file_path(&self) -> Result<PathBuf, ()>;
37 fn from_file_path<P: AsRef<Path>>(path: P) -> Result<lsp_types::Url, ()>;
38 }
39 impl UrlWasm for lsp_types::Url {
40 fn to_file_path(&self) -> Result<PathBuf, ()> {
41 Ok(self.to_string().into())
42 }
43 fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> {
44 Self::parse(path.as_ref().to_str().ok_or(())?).map_err(|_| ())
45 }
46 }
47}
48
49#[derive(Clone)]
50pub struct ServerNotifier {
51 send_notification: Function,
52 send_request: Function,
53}
54
55impl ServerNotifier {
56 pub fn send_notification(&self, method: String, params: impl Serialize) -> Result<()> {
57 self.send_notification
58 .call2(&JsValue::UNDEFINED, &method.into(), &to_value(&params)?)
59 .map_err(|x| format!("Error calling send_notification: {x:?}"))?;
60 Ok(())
61 }
62
63 pub fn send_request<T: lsp_types::request::Request>(
64 &self,
65 request: T::Params,
66 ) -> Result<impl Future<Output = Result<T::Result>>> {
67 let promise = self
68 .send_request
69 .call2(&JsValue::UNDEFINED, &T::METHOD.into(), &to_value(&request)?)
70 .map_err(|x| format!("Error calling send_request: {x:?}"))?;
71 let future = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise));
72 Ok(async move {
73 future.await.map_err(|e| format!("{e:?}").into()).and_then(|v| {
74 serde_wasm_bindgen::from_value(v).map_err(|e| format!("{e:?}").into())
75 })
76 })
77 }
78
79 pub fn send_message_to_preview(&self, message: LspToPreviewMessage) {
80 let _ = self.send_notification("slint/lsp_to_preview".to_string(), message);
81 }
82}
83
84impl RequestHandler {
85 async fn handle_request(
86 &self,
87 method: String,
88 params: JsValue,
89 ctx: Rc<Context>,
90 ) -> Result<JsValue> {
91 if let Some(f) = self.0.get(&method.as_str()) {
92 let param = serde_wasm_bindgen::from_value(params)
93 .map_err(|x| format!("invalid param to handle_request: {x:?}"))?;
94 let r = f(param, ctx).await?;
95 to_value(&r).map_err(|e| e.to_string().into())
96 } else {
97 Err("Cannot handle request".into())
98 }
99 }
100}
101
102#[derive(Default)]
103struct ReentryGuard {
104 locked: bool,
105 waker: Vec<std::task::Waker>,
106}
107
108impl ReentryGuard {
109 pub async fn lock(this: Rc<RefCell<Self>>) -> ReentryGuardLock {
110 struct ReentryGuardLocker(Rc<RefCell<ReentryGuard>>);
111
112 impl std::future::Future for ReentryGuardLocker {
113 type Output = ReentryGuardLock;
114 fn poll(
115 self: std::pin::Pin<&mut Self>,
116 cx: &mut std::task::Context<'_>,
117 ) -> std::task::Poll<Self::Output> {
118 let mut s = self.0.borrow_mut();
119 if s.locked {
120 s.waker.push(cx.waker().clone());
121 std::task::Poll::Pending
122 } else {
123 s.locked = true;
124 std::task::Poll::Ready(ReentryGuardLock(self.0.clone()))
125 }
126 }
127 }
128 ReentryGuardLocker(this).await
129 }
130}
131
132struct ReentryGuardLock(Rc<RefCell<ReentryGuard>>);
133
134impl Drop for ReentryGuardLock {
135 fn drop(&mut self) {
136 let mut s = self.0.borrow_mut();
137 s.locked = false;
138 let wakers = std::mem::take(&mut s.waker);
139 drop(s);
140 for w in wakers {
141 w.wake()
142 }
143 }
144}
145
146#[wasm_bindgen(typescript_custom_section)]
147const IMPORT_CALLBACK_FUNCTION_SECTION: &'static str = r#"
148type ImportCallbackFunction = (url: string) => Promise<string>;
149type SendRequestFunction = (method: string, r: any) => Promise<any>;
150type HighlightInPreviewFunction = (file: string, offset: number) => void;
151"#;
152
153#[wasm_bindgen]
154extern "C" {
155 #[wasm_bindgen(typescript_type = "ImportCallbackFunction")]
156 pub type ImportCallbackFunction;
157
158 #[wasm_bindgen(typescript_type = "SendRequestFunction")]
159 pub type SendRequestFunction;
160
161 #[wasm_bindgen(typescript_type = "HighlightInPreviewFunction")]
162 pub type HighlightInPreviewFunction;
163
164 // Make console.log available:
165 #[allow(unused)]
166 #[wasm_bindgen(js_namespace = console)]
167 fn log(s: &str);
168}
169
170#[wasm_bindgen]
171pub struct SlintServer {
172 ctx: Rc<Context>,
173 reentry_guard: Rc<RefCell<ReentryGuard>>,
174 rh: Rc<RequestHandler>,
175}
176
177#[wasm_bindgen]
178pub fn create(
179 init_param: JsValue,
180 send_notification: Function,
181 send_request: SendRequestFunction,
182 load_file: ImportCallbackFunction,
183) -> JsResult<SlintServer> {
184 console_error_panic_hook::set_once();
185
186 let send_request = Function::from(send_request.clone());
187 let server_notifier = ServerNotifier { send_notification, send_request };
188 let init_param = serde_wasm_bindgen::from_value(init_param)?;
189
190 let mut compiler_config =
191 CompilerConfiguration::new(i_slint_compiler::generator::OutputFormat::Interpreter);
192
193 let server_notifier_ = server_notifier.clone();
194 compiler_config.open_import_fallback = Some(Rc::new(move |path| {
195 let load_file = Function::from(load_file.clone());
196 let server_notifier = server_notifier_.clone();
197 Box::pin(async move {
198 let contents = self::load_file(path.clone(), &load_file).await;
199 let Ok(url) = Url::from_file_path(&path) else {
200 return Some(contents);
201 };
202 if let Ok(contents) = &contents {
203 server_notifier.send_message_to_preview(LspToPreviewMessage::SetContents {
204 url: VersionedUrl::new(url, None),
205 contents: contents.clone(),
206 })
207 }
208 Some(contents)
209 })
210 }));
211 let document_cache = RefCell::new(DocumentCache::new(compiler_config));
212 let reentry_guard = Rc::new(RefCell::new(ReentryGuard::default()));
213
214 let mut rh = RequestHandler::default();
215 language::register_request_handlers(&mut rh);
216
217 Ok(SlintServer {
218 ctx: Rc::new(Context {
219 document_cache,
220 init_param,
221 server_notifier,
222 to_show: Default::default(),
223 }),
224 reentry_guard,
225 rh: Rc::new(rh),
226 })
227}
228
229fn send_workspace_edit(
230 server_notifier: ServerNotifier,
231 label: Option<String>,
232 edit: Result<lsp_types::WorkspaceEdit>,
233) {
234 let Ok(edit) = edit else {
235 return;
236 };
237
238 wasm_bindgen_futures::spawn_local(async move {
239 let fut = server_notifier.send_request::<lsp_types::request::ApplyWorkspaceEdit>(
240 lsp_types::ApplyWorkspaceEditParams { label, edit },
241 );
242 if let Ok(fut) = fut {
243 // We ignore errors: If the LSP can not be reached, then all is lost
244 // anyway. The other thing that might go wrong is that our Workspace Edit
245 // refers to some outdated text. In that case the update is most likely
246 // in flight already and will cause the preview to re-render, which also
247 // invalidates all our state
248 let _ = fut.await;
249 }
250 });
251}
252
253#[wasm_bindgen]
254impl SlintServer {
255 #[cfg(all(feature = "preview-engine", feature = "preview-external"))]
256 #[wasm_bindgen]
257 pub async fn process_preview_to_lsp_message(
258 &self,
259 value: JsValue,
260 ) -> std::result::Result<(), JsValue> {
261 use crate::common::PreviewToLspMessage as M;
262
263 let guard = self.reentry_guard.clone();
264 let _lock = ReentryGuard::lock(guard).await;
265
266 let Ok(message) = serde_wasm_bindgen::from_value::<M>(value) else {
267 return Err(JsValue::from("Failed to convert value to PreviewToLspMessage"));
268 };
269
270 match message {
271 M::Status { message, health } => {
272 crate::common::lsp_to_editor::send_status_notification(
273 &self.ctx.server_notifier,
274 &message,
275 health,
276 );
277 }
278 M::Diagnostics { diagnostics, uri } => {
279 crate::common::lsp_to_editor::notify_lsp_diagnostics(
280 &self.ctx.server_notifier,
281 uri,
282 diagnostics,
283 );
284 }
285 M::ShowDocument { file, selection } => {
286 let sn = self.ctx.server_notifier.clone();
287 wasm_bindgen_futures::spawn_local(async move {
288 crate::common::lsp_to_editor::send_show_document_to_editor(sn, file, selection)
289 .await
290 });
291 }
292 M::PreviewTypeChanged { is_external: _ } => {
293 // Nothing to do!
294 }
295 M::RequestState { .. } => {
296 crate::language::request_state(&self.ctx);
297 }
298 M::UpdateElement { label, position, properties } => {
299 send_workspace_edit(
300 self.ctx.server_notifier.clone(),
301 label,
302 language::properties::update_element_properties(
303 &self.ctx, position, properties,
304 ),
305 );
306 }
307 M::SendWorkspaceEdit { label, edit } => {
308 send_workspace_edit(self.ctx.server_notifier.clone(), label, Ok(edit));
309 }
310 }
311 Ok(())
312 }
313
314 #[wasm_bindgen]
315 pub fn server_initialize_result(&self, cap: JsValue) -> JsResult<JsValue> {
316 Ok(to_value(&language::server_initialize_result(&serde_wasm_bindgen::from_value(cap)?))?)
317 }
318
319 #[wasm_bindgen]
320 pub fn reload_document(&self, content: String, uri: JsValue, version: i32) -> js_sys::Promise {
321 let ctx = self.ctx.clone();
322 let guard = self.reentry_guard.clone();
323 wasm_bindgen_futures::future_to_promise(async move {
324 let _lock = ReentryGuard::lock(guard).await;
325 let uri: lsp_types::Url = serde_wasm_bindgen::from_value(uri)?;
326 language::reload_document(
327 &ctx,
328 content,
329 uri.clone(),
330 Some(version),
331 &mut ctx.document_cache.borrow_mut(),
332 )
333 .await
334 .map_err(|e| JsError::new(&e.to_string()))?;
335 Ok(JsValue::UNDEFINED)
336 })
337 }
338
339 #[wasm_bindgen]
340 pub fn handle_request(&self, _id: JsValue, method: String, params: JsValue) -> js_sys::Promise {
341 let guard = self.reentry_guard.clone();
342 let rh = self.rh.clone();
343 let ctx = self.ctx.clone();
344 wasm_bindgen_futures::future_to_promise(async move {
345 let fut = rh.handle_request(method, params, ctx);
346 let _lock = ReentryGuard::lock(guard).await;
347 fut.await.map_err(|e| JsError::new(&e.to_string()).into())
348 })
349 }
350
351 #[wasm_bindgen]
352 pub async fn reload_config(&self) -> JsResult<()> {
353 let guard = self.reentry_guard.clone();
354 let _lock = ReentryGuard::lock(guard).await;
355 language::load_configuration(&self.ctx).await.map_err(|e| JsError::new(&e.to_string()))
356 }
357}
358
359async fn load_file(path: String, load_file: &Function) -> std::io::Result<String> {
360 let string_promise = load_file
361 .call1(&JsValue::UNDEFINED, &path.into())
362 .map_err(|x| std::io::Error::new(ErrorKind::Other, format!("{x:?}")))?;
363 let string_future = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(string_promise));
364 let js_value =
365 string_future.await.map_err(|e| std::io::Error::new(ErrorKind::Other, format!("{e:?}")))?;
366 return Ok(js_value.as_string().unwrap_or_default());
367}
368
369// Use a JSON friendly representation to avoid using ES maps instead of JS objects.
370fn to_value<T: serde::Serialize + ?Sized>(
371 value: &T,
372) -> std::result::Result<wasm_bindgen::JsValue, serde_wasm_bindgen::Error> {
373 value.serialize(&serde_wasm_bindgen::Serializer::json_compatible())
374}
375