1#![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 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(¶ms)?)
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 #[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 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 }
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
434fn 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}