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 | |
6 | pub mod common; |
7 | mod fmt; |
8 | mod language; |
9 | pub mod lsp_ext; |
10 | #[cfg (feature = "preview-engine" )] |
11 | mod preview; |
12 | pub mod util; |
13 | |
14 | use common::{LspToPreviewMessage, Result, VersionedUrl}; |
15 | use i_slint_compiler::CompilerConfiguration; |
16 | use js_sys::Function; |
17 | pub use language::{Context, DocumentCache, RequestHandler}; |
18 | use lsp_types::Url; |
19 | use serde::Serialize; |
20 | use std::cell::RefCell; |
21 | use std::future::Future; |
22 | use std::io::ErrorKind; |
23 | use std::rc::Rc; |
24 | use wasm_bindgen::prelude::*; |
25 | |
26 | #[cfg (target_arch = "wasm32" )] |
27 | use crate::wasm_prelude::*; |
28 | |
29 | type JsResult<T> = std::result::Result<T, JsError>; |
30 | |
31 | pub 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)] |
50 | pub struct ServerNotifier { |
51 | send_notification: Function, |
52 | send_request: Function, |
53 | } |
54 | |
55 | impl 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(¶ms)?) |
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 | |
84 | impl 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)] |
103 | struct ReentryGuard { |
104 | locked: bool, |
105 | waker: Vec<std::task::Waker>, |
106 | } |
107 | |
108 | impl 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 | |
132 | struct ReentryGuardLock(Rc<RefCell<ReentryGuard>>); |
133 | |
134 | impl 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)] |
147 | const IMPORT_CALLBACK_FUNCTION_SECTION: &'static str = r#" |
148 | type ImportCallbackFunction = (url: string) => Promise<string>; |
149 | type SendRequestFunction = (method: string, r: any) => Promise<any>; |
150 | type HighlightInPreviewFunction = (file: string, offset: number) => void; |
151 | "# ; |
152 | |
153 | #[wasm_bindgen] |
154 | extern "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] |
171 | pub struct SlintServer { |
172 | ctx: Rc<Context>, |
173 | reentry_guard: Rc<RefCell<ReentryGuard>>, |
174 | rh: Rc<RequestHandler>, |
175 | } |
176 | |
177 | #[wasm_bindgen] |
178 | pub 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 | |
229 | fn 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] |
254 | impl 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 | |
359 | async 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. |
370 | fn 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 | |