| 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 | |
| 7 | pub mod common; |
| 8 | mod fmt; |
| 9 | mod language; |
| 10 | #[cfg (feature = "preview-engine" )] |
| 11 | mod preview; |
| 12 | pub mod util; |
| 13 | |
| 14 | use common::{DocumentCache, LspToPreviewMessage, Result, VersionedUrl}; |
| 15 | use js_sys::Function; |
| 16 | pub use language::{Context, RequestHandler}; |
| 17 | use lsp_types::Url; |
| 18 | use std::cell::RefCell; |
| 19 | use std::future::Future; |
| 20 | use std::io::ErrorKind; |
| 21 | use std::rc::Rc; |
| 22 | use wasm_bindgen::prelude::*; |
| 23 | |
| 24 | #[cfg (target_arch = "wasm32" )] |
| 25 | use crate::wasm_prelude::*; |
| 26 | |
| 27 | type JsResult<T> = std::result::Result<T, JsError>; |
| 28 | |
| 29 | pub 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)] |
| 48 | pub struct ServerNotifier { |
| 49 | send_notification: Function, |
| 50 | send_request: Function, |
| 51 | } |
| 52 | |
| 53 | impl 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 | pub fn send_message_to_preview(&self, message: LspToPreviewMessage) { |
| 81 | let _ = self.send_notification::<LspToPreviewMessage>(message); |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | impl 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)] |
| 104 | struct ReentryGuard { |
| 105 | locked: bool, |
| 106 | waker: Vec<std::task::Waker>, |
| 107 | } |
| 108 | |
| 109 | impl 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 | |
| 133 | struct ReentryGuardLock(Rc<RefCell<ReentryGuard>>); |
| 134 | |
| 135 | impl 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)] |
| 148 | const IMPORT_CALLBACK_FUNCTION_SECTION: &'static str = r#" |
| 149 | type ImportCallbackFunction = (url: string) => Promise<string>; |
| 150 | type SendRequestFunction = (method: string, r: any) => Promise<any>; |
| 151 | type HighlightInPreviewFunction = (file: string, offset: number) => void; |
| 152 | "# ; |
| 153 | |
| 154 | #[wasm_bindgen] |
| 155 | extern "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] |
| 172 | pub struct SlintServer { |
| 173 | ctx: Rc<Context>, |
| 174 | reentry_guard: Rc<RefCell<ReentryGuard>>, |
| 175 | rh: Rc<RequestHandler>, |
| 176 | } |
| 177 | |
| 178 | #[wasm_bindgen] |
| 179 | pub 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 | |
| 231 | fn 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] |
| 256 | impl 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 | |
| 413 | async 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. |
| 424 | fn 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 | |