slint_lsp_wasm/
wasm_main.rs

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