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, 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 pub fn send_message_to_preview(&self, message: LspToPreviewMessage) {
81 let _ = self.send_notification::<LspToPreviewMessage>(message);
82 }
83}
84
85impl RequestHandler {
86 async fn handle_request(
87 &self,
88 method: String,
89 params: JsValue,
90 ctx: Rc<Context>,
91 ) -> Result<JsValue> {
92 if let Some(f) = self.0.get(&method.as_str()) {
93 let param = serde_wasm_bindgen::from_value(params)
94 .map_err(|x| format!("invalid param to handle_request: {x:?}"))?;
95 let r = f(param, ctx).await.map_err(|e| e.message)?;
96 to_value(&r).map_err(|e| e.to_string().into())
97 } else {
98 Err("Cannot handle request".into())
99 }
100 }
101}
102
103#[derive(Default)]
104struct ReentryGuard {
105 locked: bool,
106 waker: Vec<std::task::Waker>,
107}
108
109impl ReentryGuard {
110 pub async fn lock(this: Rc<RefCell<Self>>) -> ReentryGuardLock {
111 struct ReentryGuardLocker(Rc<RefCell<ReentryGuard>>);
112
113 impl std::future::Future for ReentryGuardLocker {
114 type Output = ReentryGuardLock;
115 fn poll(
116 self: std::pin::Pin<&mut Self>,
117 cx: &mut std::task::Context<'_>,
118 ) -> std::task::Poll<Self::Output> {
119 let mut s = self.0.borrow_mut();
120 if s.locked {
121 s.waker.push(cx.waker().clone());
122 std::task::Poll::Pending
123 } else {
124 s.locked = true;
125 std::task::Poll::Ready(ReentryGuardLock(self.0.clone()))
126 }
127 }
128 }
129 ReentryGuardLocker(this).await
130 }
131}
132
133struct ReentryGuardLock(Rc<RefCell<ReentryGuard>>);
134
135impl Drop for ReentryGuardLock {
136 fn drop(&mut self) {
137 let mut s = self.0.borrow_mut();
138 s.locked = false;
139 let wakers = std::mem::take(&mut s.waker);
140 drop(s);
141 for w in wakers {
142 w.wake()
143 }
144 }
145}
146
147#[wasm_bindgen(typescript_custom_section)]
148const IMPORT_CALLBACK_FUNCTION_SECTION: &'static str = r#"
149type ImportCallbackFunction = (url: string) => Promise<string>;
150type SendRequestFunction = (method: string, r: any) => Promise<any>;
151type HighlightInPreviewFunction = (file: string, offset: number) => void;
152"#;
153
154#[wasm_bindgen]
155extern "C" {
156 #[wasm_bindgen(typescript_type = "ImportCallbackFunction")]
157 pub type ImportCallbackFunction;
158
159 #[wasm_bindgen(typescript_type = "SendRequestFunction")]
160 pub type SendRequestFunction;
161
162 #[wasm_bindgen(typescript_type = "HighlightInPreviewFunction")]
163 pub type HighlightInPreviewFunction;
164
165 // Make console.log available:
166 #[allow(unused)]
167 #[wasm_bindgen(js_namespace = console)]
168 fn log(s: &str);
169}
170
171#[wasm_bindgen]
172pub struct SlintServer {
173 ctx: Rc<Context>,
174 reentry_guard: Rc<RefCell<ReentryGuard>>,
175 rh: Rc<RequestHandler>,
176}
177
178#[wasm_bindgen]
179pub fn create(
180 init_param: JsValue,
181 send_notification: Function,
182 send_request: SendRequestFunction,
183 load_file: ImportCallbackFunction,
184) -> JsResult<SlintServer> {
185 console_error_panic_hook::set_once();
186
187 let send_request = Function::from(send_request.clone());
188 let server_notifier = ServerNotifier { send_notification, send_request };
189 let init_param = serde_wasm_bindgen::from_value(init_param)?;
190
191 let mut compiler_config = crate::common::document_cache::CompilerConfiguration::default();
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.map(|c| (None, c)));
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.map(|c| (None, c)))
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 preview_config: RefCell::new(Default::default()),
221 init_param,
222 server_notifier,
223 to_show: Default::default(),
224 open_urls: Default::default(),
225 }),
226 reentry_guard,
227 rh: Rc::new(rh),
228 })
229}
230
231fn send_workspace_edit(
232 server_notifier: ServerNotifier,
233 label: Option<String>,
234 edit: Result<lsp_types::WorkspaceEdit>,
235) {
236 let Ok(edit) = edit else {
237 return;
238 };
239
240 wasm_bindgen_futures::spawn_local(async move {
241 let fut = server_notifier.send_request::<lsp_types::request::ApplyWorkspaceEdit>(
242 lsp_types::ApplyWorkspaceEditParams { label, edit },
243 );
244 if let Ok(fut) = fut {
245 // We ignore errors: If the LSP can not be reached, then all is lost
246 // anyway. The other thing that might go wrong is that our Workspace Edit
247 // refers to some outdated text. In that case the update is most likely
248 // in flight already and will cause the preview to re-render, which also
249 // invalidates all our state
250 let _ = fut.await;
251 }
252 });
253}
254
255#[wasm_bindgen]
256impl SlintServer {
257 #[cfg(all(feature = "preview-engine", feature = "preview-external"))]
258 #[wasm_bindgen]
259 pub async fn process_preview_to_lsp_message(
260 &self,
261 value: JsValue,
262 ) -> std::result::Result<(), JsValue> {
263 use crate::common::PreviewToLspMessage as M;
264
265 let guard = self.reentry_guard.clone();
266 let _lock = ReentryGuard::lock(guard).await;
267
268 let Ok(message) = serde_wasm_bindgen::from_value::<M>(value) else {
269 return Err(JsValue::from("Failed to convert value to PreviewToLspMessage"));
270 };
271
272 match message {
273 M::Diagnostics { diagnostics, version, uri } => {
274 crate::common::lsp_to_editor::notify_lsp_diagnostics(
275 &self.ctx.server_notifier,
276 uri,
277 version,
278 diagnostics,
279 );
280 }
281 M::ShowDocument { file, selection, .. } => {
282 let sn = self.ctx.server_notifier.clone();
283 wasm_bindgen_futures::spawn_local(async move {
284 crate::common::lsp_to_editor::send_show_document_to_editor(
285 sn, file, selection, true,
286 )
287 .await
288 });
289 }
290 M::PreviewTypeChanged { is_external: _ } => {
291 // Nothing to do!
292 }
293 M::RequestState { .. } => {
294 crate::language::request_state(&self.ctx);
295 }
296 M::SendWorkspaceEdit { label, edit } => {
297 send_workspace_edit(self.ctx.server_notifier.clone(), label, Ok(edit));
298 }
299 M::SendShowMessage { message } => {
300 let _ = self
301 .ctx
302 .server_notifier
303 .send_notification::<lsp_types::notification::ShowMessage>(message);
304 }
305 }
306 Ok(())
307 }
308
309 #[wasm_bindgen]
310 pub fn server_initialize_result(&self, cap: JsValue) -> JsResult<JsValue> {
311 Ok(to_value(&language::server_initialize_result(&serde_wasm_bindgen::from_value(cap)?))?)
312 }
313
314 #[wasm_bindgen]
315 pub async fn startup_lsp(&self) -> js_sys::Promise {
316 let ctx = self.ctx.clone();
317 let guard = self.reentry_guard.clone();
318 wasm_bindgen_futures::future_to_promise(async move {
319 let _lock = ReentryGuard::lock(guard).await;
320 language::startup_lsp(&ctx).await.map_err(|e| JsError::new(&e.to_string()))?;
321 Ok(JsValue::UNDEFINED)
322 })
323 }
324
325 #[wasm_bindgen]
326 pub fn trigger_file_watcher(&self, url: JsValue, typ: JsValue) -> js_sys::Promise {
327 let ctx = self.ctx.clone();
328 let guard = self.reentry_guard.clone();
329
330 wasm_bindgen_futures::future_to_promise(async move {
331 let _lock = ReentryGuard::lock(guard).await;
332 let url: lsp_types::Url = serde_wasm_bindgen::from_value(url)?;
333 let typ: lsp_types::FileChangeType = serde_wasm_bindgen::from_value(typ)?;
334 language::trigger_file_watcher(&ctx, url, typ)
335 .await
336 .map_err(|e| JsError::new(&e.to_string()))?;
337 Ok(JsValue::UNDEFINED)
338 })
339 }
340
341 #[wasm_bindgen]
342 pub fn open_document(&self, content: String, uri: JsValue, version: i32) -> js_sys::Promise {
343 let ctx = self.ctx.clone();
344 let guard = self.reentry_guard.clone();
345 wasm_bindgen_futures::future_to_promise(async move {
346 let _lock = ReentryGuard::lock(guard).await;
347 let uri: lsp_types::Url = serde_wasm_bindgen::from_value(uri)?;
348 language::open_document(
349 &ctx,
350 content,
351 uri.clone(),
352 Some(version),
353 &mut ctx.document_cache.borrow_mut(),
354 )
355 .await
356 .map_err(|e| JsError::new(&e.to_string()))?;
357 Ok(JsValue::UNDEFINED)
358 })
359 }
360
361 #[wasm_bindgen]
362 pub fn reload_document(&self, content: String, uri: JsValue, version: i32) -> js_sys::Promise {
363 let ctx = self.ctx.clone();
364 let guard = self.reentry_guard.clone();
365 wasm_bindgen_futures::future_to_promise(async move {
366 let _lock = ReentryGuard::lock(guard).await;
367 let uri: lsp_types::Url = serde_wasm_bindgen::from_value(uri)?;
368 language::reload_document(
369 &ctx,
370 content,
371 uri.clone(),
372 Some(version),
373 &mut ctx.document_cache.borrow_mut(),
374 )
375 .await
376 .map_err(|e| JsError::new(&e.to_string()))?;
377 Ok(JsValue::UNDEFINED)
378 })
379 }
380
381 #[wasm_bindgen]
382 pub fn close_document(&self, uri: JsValue) -> js_sys::Promise {
383 let ctx = self.ctx.clone();
384 let guard = self.reentry_guard.clone();
385 wasm_bindgen_futures::future_to_promise(async move {
386 let _lock = ReentryGuard::lock(guard).await;
387 let uri: lsp_types::Url = serde_wasm_bindgen::from_value(uri)?;
388 language::close_document(&ctx, uri).await.map_err(|e| JsError::new(&e.to_string()))?;
389 Ok(JsValue::UNDEFINED)
390 })
391 }
392
393 #[wasm_bindgen]
394 pub fn handle_request(&self, _id: JsValue, method: String, params: JsValue) -> js_sys::Promise {
395 let guard = self.reentry_guard.clone();
396 let rh = self.rh.clone();
397 let ctx = self.ctx.clone();
398 wasm_bindgen_futures::future_to_promise(async move {
399 let fut = rh.handle_request(method, params, ctx);
400 let _lock = ReentryGuard::lock(guard).await;
401 fut.await.map_err(|e| JsError::new(&e.to_string()).into())
402 })
403 }
404
405 #[wasm_bindgen]
406 pub async fn reload_config(&self) -> JsResult<()> {
407 let guard = self.reentry_guard.clone();
408 let _lock = ReentryGuard::lock(guard).await;
409 language::load_configuration(&self.ctx).await.map_err(|e| JsError::new(&e.to_string()))
410 }
411}
412
413async fn load_file(path: String, load_file: &Function) -> std::io::Result<String> {
414 let string_promise = load_file
415 .call1(&JsValue::UNDEFINED, &path.into())
416 .map_err(|x| std::io::Error::new(ErrorKind::Other, format!("{x:?}")))?;
417 let string_future = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(string_promise));
418 let js_value =
419 string_future.await.map_err(|e| std::io::Error::new(ErrorKind::Other, format!("{e:?}")))?;
420 return Ok(js_value.as_string().unwrap_or_default());
421}
422
423// Use a JSON friendly representation to avoid using ES maps instead of JS objects.
424fn to_value<T: serde::Serialize + ?Sized>(
425 value: &T,
426) -> std::result::Result<wasm_bindgen::JsValue, serde_wasm_bindgen::Error> {
427 value.serialize(&serde_wasm_bindgen::Serializer::json_compatible())
428}
429