oo-api 0.0.4

Api support for ∞ extensions
Documentation
//! oo extension API — link this into your WASM extension.
//!
//! # Minimal extension
//!
//! ```rust,ignore
//! #[no_mangle]
//! pub extern "C" fn oo_init() {}
//!
//! #[no_mangle]
//! pub extern "C" fn detect_project() -> i32 { 1 }
//!
//! #[no_mangle]
//! pub extern "C" fn oo_event(ptr: i32, len: i32) {
//!     oo_api::show_notification("Hello from WASM!", "info");
//! }
//! ```

#![cfg_attr(target_arch = "wasm32", no_std)]

#[cfg(target_arch = "wasm32")]
extern crate alloc;

// On non-wasm targets re-export std as alloc so the rest of the file compiles.
#[cfg(not(target_arch = "wasm32"))]
extern crate std as alloc;

use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use core::future::Future;
use core::pin::Pin;
use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

use oo_protocol::{
    CommandOp, HostRequest, Message, Operation, Request, Response, ResponsePayload, TerminalOp,
    WindowOp, WorkspaceOp,
};

// ---------------------------------------------------------------------------
// Allocator exports — host uses these to write into WASM linear memory.
// ---------------------------------------------------------------------------

/// oo_alloc for use in a wasm extension
/// # Safety
/// exclusively intended for use in extension
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oo_alloc(size: i32) -> i32 {
    let layout = core::alloc::Layout::from_size_align(size as usize, 1).unwrap();
    let ptr = unsafe { alloc::alloc::alloc(layout) };
    ptr as i32
}

/// oo_free for use in a wasm extension
/// # Safety
/// exclusively intended for use in extension
#[unsafe(no_mangle)]
pub unsafe extern "C" fn oo_free(ptr: i32, size: i32) {
    let layout = core::alloc::Layout::from_size_align(size as usize, 1).unwrap();
    unsafe { alloc::alloc::dealloc(ptr as *mut u8, layout) };
}

// ---------------------------------------------------------------------------
// Host ABI import
// ---------------------------------------------------------------------------

#[link(wasm_import_module = "oo")]
unsafe extern "C" {
    /// Single-function host ABI.  Sends a MessagePack-encoded `Message::Request`
    /// to the host.  The host may respond synchronously by calling `oo_on_message`
    /// before returning.
    fn __oo_host_call(ptr: i32, len: i32);
}

// ---------------------------------------------------------------------------
// Async runtime (single-threaded, no_std-compatible)
// ---------------------------------------------------------------------------

struct Pending {
    result: Option<ResponsePayload>,
    waker: Option<Waker>,
}

static mut NEXT_ID: u32 = 1;
static mut PENDING: Option<BTreeMap<u32, Pending>> = None;

fn pending_map() -> &'static mut BTreeMap<u32, Pending> {
    unsafe {
        let p = core::ptr::addr_of_mut!(PENDING);
        if (*p).is_none() {
            *p = Some(BTreeMap::new());
        }
        (*p).as_mut().unwrap()
    }
}

fn next_id() -> u32 {
    unsafe {
        let p = core::ptr::addr_of_mut!(NEXT_ID);
        let id = *p;
        *p = id.wrapping_add(1);
        id
    }
}

/// Send a request to the host and return a future that resolves when the host
/// calls back via `oo_on_message`.
pub fn send_request(payload: HostRequest) -> ResponseFuture {
    let id = next_id();
    pending_map().insert(
        id,
        Pending {
            result: None,
            waker: None,
        },
    );

    let msg = Message::Request(Request { id, payload });
    let bytes = rmp_serde::to_vec(&msg).unwrap_or_default();

    unsafe { __oo_host_call(bytes.as_ptr() as i32, bytes.len() as i32) };

    ResponseFuture { id }
}

/// Future returned by `send_request`.
pub struct ResponseFuture {
    id: u32,
}

impl Future for ResponseFuture {
    type Output = ResponsePayload;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let map = pending_map();
        if let Some(entry) = map.get_mut(&self.id) {
            if let Some(result) = entry.result.take() {
                map.remove(&self.id);
                return Poll::Ready(result);
            }
            entry.waker = Some(cx.waker().clone());
        }
        Poll::Pending
    }
}

// ---------------------------------------------------------------------------
// `oo_on_message` — host calls this to deliver a response
// ---------------------------------------------------------------------------

/// Called by the host to deliver a response.  The payload is a
/// MessagePack-encoded `Message::Response` in WASM linear memory.
#[unsafe(no_mangle)]
pub extern "C" fn oo_on_message(ptr: i32, len: i32) {
    let bytes = unsafe { core::slice::from_raw_parts(ptr as *const u8, len as usize) };
    let msg: Message = match rmp_serde::from_slice(bytes) {
        Ok(m) => m,
        Err(_) => return,
    };
    if let Message::Response(Response { id, result }) = msg
        && let Some(entry) = pending_map().get_mut(&id)
    {
        entry.result = Some(result);
        if let Some(waker) = entry.waker.take() {
            waker.wake();
        }
    }
}

// ---------------------------------------------------------------------------
// Minimal block_on executor
// ---------------------------------------------------------------------------

/// Spin-polls `f` until it resolves.  Safe for single-threaded WASM where the
/// host drives re-entry via `oo_on_message` from within `__oo_host_call`.
pub fn block_on<F: Future>(mut f: F) -> F::Output {
    // Safety: we never move f after pinning.
    let mut f = unsafe { Pin::new_unchecked(&mut f) };
    let waker = noop_waker();
    let mut cx = Context::from_waker(&waker);
    loop {
        match f.as_mut().poll(&mut cx) {
            Poll::Ready(v) => return v,
            Poll::Pending => {}
        }
    }
}

fn noop_waker() -> Waker {
    const VTABLE: RawWakerVTable =
        RawWakerVTable::new(|p| RawWaker::new(p, &VTABLE), |_| {}, |_| {}, |_| {});
    unsafe { Waker::from_raw(RawWaker::new(core::ptr::null(), &VTABLE)) }
}

// ---------------------------------------------------------------------------
// Public API helpers
// ---------------------------------------------------------------------------

/// Show a user-visible notification.  `level` is `"info"`, `"warn"`, or `"error"`.
pub fn show_notification(message: &str, level: &str) {
    block_on(send_request(HostRequest::Operation(Operation::Window(
        WindowOp::ShowNotification {
            message: message.to_string(),
            level: level.to_string(),
        },
    ))));
}

/// Ask the host to open a file in the editor.
pub fn open_file(path: &str) {
    block_on(send_request(HostRequest::Operation(Operation::Workspace(
        WorkspaceOp::OpenFile {
            path: path.to_string(),
        },
    ))));
}

/// Update the status bar text.  `slot` is `"left"`, `"center"`, or `"right"`.
pub fn update_status_bar(text: &str, slot: Option<&str>) {
    block_on(send_request(HostRequest::Operation(Operation::Window(
        WindowOp::UpdateStatusBar {
            text: text.to_string(),
            slot: slot.map(|s| s.to_string()),
        },
    ))));
}

/// Create a terminal panel running `command`.
pub fn create_terminal(command: &str) {
    block_on(send_request(HostRequest::Operation(Operation::Terminal(
        TerminalOp::Create {
            command: command.to_string(),
        },
    ))));
}

/// Trigger an IDE command by its dotted ID string (e.g. `"editor.save"`).
pub fn run_command(id: &str) {
    block_on(send_request(HostRequest::Operation(Operation::Command(
        CommandOp::Run { id: id.to_string() },
    ))));
}

/// Write a message to the IDE log.
pub fn log(message: &str) {
    block_on(send_request(HostRequest::Log {
        message: message.to_string(),
    }));
}

/// Read a config value for this extension.  Returns `None` if not found.
///
/// This call is synchronous: the host responds inline before returning from
/// `__oo_host_call`, so `block_on` resolves in a single poll.
pub fn get_config(key: &str) -> Option<String> {
    let future = send_request(HostRequest::GetConfig {
        key: key.to_string(),
    });
    match block_on(future) {
        ResponsePayload::String(s) => Some(s),
        _ => None,
    }
}

/// Returns true when the host has detected at least one file of `lang_id` in
/// the project.  The host exposes these via a dedicated `HasLanguage` request;
/// this helper sends that request and interprets a "true" string response.
pub fn has_language(lang_id: &str) -> bool {
    let future = send_request(HostRequest::HasLanguage { lang: lang_id.to_string() });
    match block_on(future) {
        ResponsePayload::String(s) => s == "true",
        _ => false,
    }
}


#[cfg(test)]
pub fn __test_inject_response(resp: ResponsePayload) {
    let id = *pending_map().keys().last().unwrap();
    if let Some(entry) = pending_map().get_mut(&id) {
        entry.result = Some(resp);
        if let Some(w) = entry.waker.take() {
            w.wake();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicUsize, Ordering};

    static TEST_MODE: AtomicUsize = AtomicUsize::new(0);

    #[unsafe(no_mangle)]
    extern "C" fn __oo_host_call(_ptr: i32, _len: i32) {
        let mode = TEST_MODE.load(Ordering::SeqCst);
        let val = match mode {
            1 => Some("true"),
            2 => Some("false"),
            _ => None,
        };
        if let Some(s) = val {
            let id = *pending_map().keys().last().unwrap();
            if let Some(entry) = pending_map().get_mut(&id) {
                entry.result = Some(ResponsePayload::String(s.to_string()));
                if let Some(w) = entry.waker.take() {
                    w.wake();
                }
            }
        }
    }

    #[test]
    fn has_language_true() {
        TEST_MODE.store(1, Ordering::SeqCst);
        assert!(has_language("rust"));
    }

    #[test]
    fn has_language_false() {
        TEST_MODE.store(2, Ordering::SeqCst);
        assert!(!has_language("python"));
    }
}