Skip to main content

oo_api/
lib.rs

1//! oo extension API — link this into your WASM extension.
2//!
3//! # Minimal extension
4//!
5//! ```rust,ignore
6//! #[no_mangle]
7//! pub extern "C" fn oo_init() {}
8//!
9//! #[no_mangle]
10//! pub extern "C" fn detect_project() -> i32 { 1 }
11//!
12//! #[no_mangle]
13//! pub extern "C" fn oo_event(ptr: i32, len: i32) {
14//!     oo_api::show_notification("Hello from WASM!", "info");
15//! }
16//! ```
17
18#![cfg_attr(target_arch = "wasm32", no_std)]
19
20#[cfg(target_arch = "wasm32")]
21extern crate alloc;
22
23// On non-wasm targets re-export std as alloc so the rest of the file compiles.
24#[cfg(not(target_arch = "wasm32"))]
25extern crate std as alloc;
26
27use alloc::collections::BTreeMap;
28use alloc::string::{String, ToString};
29use core::future::Future;
30use core::pin::Pin;
31use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
32
33use oo_protocol::{
34    CommandOp, HostRequest, Message, Operation, Request, Response, ResponsePayload, TerminalOp,
35    WindowOp, WorkspaceOp,
36};
37
38// ---------------------------------------------------------------------------
39// Allocator exports — host uses these to write into WASM linear memory.
40// ---------------------------------------------------------------------------
41
42/// oo_alloc for use in a wasm extension
43/// # Safety
44/// exclusively intended for use in extension
45#[unsafe(no_mangle)]
46pub unsafe extern "C" fn oo_alloc(size: i32) -> i32 {
47    let layout = core::alloc::Layout::from_size_align(size as usize, 1).unwrap();
48    let ptr = unsafe { alloc::alloc::alloc(layout) };
49    ptr as i32
50}
51
52/// oo_free for use in a wasm extension
53/// # Safety
54/// exclusively intended for use in extension
55#[unsafe(no_mangle)]
56pub unsafe extern "C" fn oo_free(ptr: i32, size: i32) {
57    let layout = core::alloc::Layout::from_size_align(size as usize, 1).unwrap();
58    unsafe { alloc::alloc::dealloc(ptr as *mut u8, layout) };
59}
60
61// ---------------------------------------------------------------------------
62// Host ABI import
63// ---------------------------------------------------------------------------
64
65#[link(wasm_import_module = "oo")]
66unsafe extern "C" {
67    /// Single-function host ABI.  Sends a MessagePack-encoded `Message::Request`
68    /// to the host.  The host may respond synchronously by calling `oo_on_message`
69    /// before returning.
70    fn __oo_host_call(ptr: i32, len: i32);
71}
72
73// ---------------------------------------------------------------------------
74// Async runtime (single-threaded, no_std-compatible)
75// ---------------------------------------------------------------------------
76
77struct Pending {
78    result: Option<ResponsePayload>,
79    waker: Option<Waker>,
80}
81
82static mut NEXT_ID: u32 = 1;
83static mut PENDING: Option<BTreeMap<u32, Pending>> = None;
84
85fn pending_map() -> &'static mut BTreeMap<u32, Pending> {
86    unsafe {
87        let p = core::ptr::addr_of_mut!(PENDING);
88        if (*p).is_none() {
89            *p = Some(BTreeMap::new());
90        }
91        (*p).as_mut().unwrap()
92    }
93}
94
95fn next_id() -> u32 {
96    unsafe {
97        let p = core::ptr::addr_of_mut!(NEXT_ID);
98        let id = *p;
99        *p = id.wrapping_add(1);
100        id
101    }
102}
103
104/// Send a request to the host and return a future that resolves when the host
105/// calls back via `oo_on_message`.
106pub fn send_request(payload: HostRequest) -> ResponseFuture {
107    let id = next_id();
108    pending_map().insert(
109        id,
110        Pending {
111            result: None,
112            waker: None,
113        },
114    );
115
116    let msg = Message::Request(Request { id, payload });
117    let bytes = rmp_serde::to_vec(&msg).unwrap_or_default();
118
119    unsafe { __oo_host_call(bytes.as_ptr() as i32, bytes.len() as i32) };
120
121    ResponseFuture { id }
122}
123
124/// Future returned by `send_request`.
125pub struct ResponseFuture {
126    id: u32,
127}
128
129impl Future for ResponseFuture {
130    type Output = ResponsePayload;
131
132    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
133        let map = pending_map();
134        if let Some(entry) = map.get_mut(&self.id) {
135            if let Some(result) = entry.result.take() {
136                map.remove(&self.id);
137                return Poll::Ready(result);
138            }
139            entry.waker = Some(cx.waker().clone());
140        }
141        Poll::Pending
142    }
143}
144
145// ---------------------------------------------------------------------------
146// `oo_on_message` — host calls this to deliver a response
147// ---------------------------------------------------------------------------
148
149/// Called by the host to deliver a response.  The payload is a
150/// MessagePack-encoded `Message::Response` in WASM linear memory.
151#[unsafe(no_mangle)]
152pub extern "C" fn oo_on_message(ptr: i32, len: i32) {
153    let bytes = unsafe { core::slice::from_raw_parts(ptr as *const u8, len as usize) };
154    let msg: Message = match rmp_serde::from_slice(bytes) {
155        Ok(m) => m,
156        Err(_) => return,
157    };
158    if let Message::Response(Response { id, result }) = msg
159        && let Some(entry) = pending_map().get_mut(&id)
160    {
161        entry.result = Some(result);
162        if let Some(waker) = entry.waker.take() {
163            waker.wake();
164        }
165    }
166}
167
168// ---------------------------------------------------------------------------
169// Minimal block_on executor
170// ---------------------------------------------------------------------------
171
172/// Spin-polls `f` until it resolves.  Safe for single-threaded WASM where the
173/// host drives re-entry via `oo_on_message` from within `__oo_host_call`.
174pub fn block_on<F: Future>(mut f: F) -> F::Output {
175    // Safety: we never move f after pinning.
176    let mut f = unsafe { Pin::new_unchecked(&mut f) };
177    let waker = noop_waker();
178    let mut cx = Context::from_waker(&waker);
179    loop {
180        match f.as_mut().poll(&mut cx) {
181            Poll::Ready(v) => return v,
182            Poll::Pending => {}
183        }
184    }
185}
186
187fn noop_waker() -> Waker {
188    const VTABLE: RawWakerVTable =
189        RawWakerVTable::new(|p| RawWaker::new(p, &VTABLE), |_| {}, |_| {}, |_| {});
190    unsafe { Waker::from_raw(RawWaker::new(core::ptr::null(), &VTABLE)) }
191}
192
193// ---------------------------------------------------------------------------
194// Public API helpers
195// ---------------------------------------------------------------------------
196
197/// Show a user-visible notification.  `level` is `"info"`, `"warn"`, or `"error"`.
198pub fn show_notification(message: &str, level: &str) {
199    block_on(send_request(HostRequest::Operation(Operation::Window(
200        WindowOp::ShowNotification {
201            message: message.to_string(),
202            level: level.to_string(),
203        },
204    ))));
205}
206
207/// Ask the host to open a file in the editor.
208pub fn open_file(path: &str) {
209    block_on(send_request(HostRequest::Operation(Operation::Workspace(
210        WorkspaceOp::OpenFile {
211            path: path.to_string(),
212        },
213    ))));
214}
215
216/// Update the status bar text.  `slot` is `"left"`, `"center"`, or `"right"`.
217pub fn update_status_bar(text: &str, slot: Option<&str>) {
218    block_on(send_request(HostRequest::Operation(Operation::Window(
219        WindowOp::UpdateStatusBar {
220            text: text.to_string(),
221            slot: slot.map(|s| s.to_string()),
222        },
223    ))));
224}
225
226/// Create a terminal panel running `command`.
227pub fn create_terminal(command: &str) {
228    block_on(send_request(HostRequest::Operation(Operation::Terminal(
229        TerminalOp::Create {
230            command: command.to_string(),
231        },
232    ))));
233}
234
235/// Trigger an IDE command by its dotted ID string (e.g. `"editor.save"`).
236pub fn run_command(id: &str) {
237    block_on(send_request(HostRequest::Operation(Operation::Command(
238        CommandOp::Run { id: id.to_string() },
239    ))));
240}
241
242/// Write a message to the IDE log.
243pub fn log(message: &str) {
244    block_on(send_request(HostRequest::Log {
245        message: message.to_string(),
246    }));
247}
248
249/// Read a config value for this extension.  Returns `None` if not found.
250///
251/// This call is synchronous: the host responds inline before returning from
252/// `__oo_host_call`, so `block_on` resolves in a single poll.
253pub fn get_config(key: &str) -> Option<String> {
254    let future = send_request(HostRequest::GetConfig {
255        key: key.to_string(),
256    });
257    match block_on(future) {
258        ResponsePayload::String(s) => Some(s),
259        _ => None,
260    }
261}
262
263/// Returns true when the host has detected at least one file of `lang_id` in
264/// the project.  The host exposes these via a dedicated `HasLanguage` request;
265/// this helper sends that request and interprets a "true" string response.
266pub fn has_language(lang_id: &str) -> bool {
267    let future = send_request(HostRequest::HasLanguage { lang: lang_id.to_string() });
268    match block_on(future) {
269        ResponsePayload::String(s) => s == "true",
270        _ => false,
271    }
272}
273
274
275#[cfg(test)]
276pub fn __test_inject_response(resp: ResponsePayload) {
277    let id = *pending_map().keys().last().unwrap();
278    if let Some(entry) = pending_map().get_mut(&id) {
279        entry.result = Some(resp);
280        if let Some(w) = entry.waker.take() {
281            w.wake();
282        }
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use std::sync::atomic::{AtomicUsize, Ordering};
290
291    static TEST_MODE: AtomicUsize = AtomicUsize::new(0);
292
293    #[unsafe(no_mangle)]
294    extern "C" fn __oo_host_call(_ptr: i32, _len: i32) {
295        let mode = TEST_MODE.load(Ordering::SeqCst);
296        let val = match mode {
297            1 => Some("true"),
298            2 => Some("false"),
299            _ => None,
300        };
301        if let Some(s) = val {
302            let id = *pending_map().keys().last().unwrap();
303            if let Some(entry) = pending_map().get_mut(&id) {
304                entry.result = Some(ResponsePayload::String(s.to_string()));
305                if let Some(w) = entry.waker.take() {
306                    w.wake();
307                }
308            }
309        }
310    }
311
312    #[test]
313    fn has_language_true() {
314        TEST_MODE.store(1, Ordering::SeqCst);
315        assert!(has_language("rust"));
316    }
317
318    #[test]
319    fn has_language_false() {
320        TEST_MODE.store(2, Ordering::SeqCst);
321        assert!(!has_language("python"));
322    }
323}
324