Files
Decodal/crates/decodal-wasm/src/lib.rs
T

264 lines
8.5 KiB
Rust

#[cfg(target_arch = "wasm32")]
use decodal::HostEnvironment;
use decodal::{Data, EmptyLoader, Engine, SourceId, format_diagnostic_with};
#[cfg(target_arch = "wasm32")]
use decodal_language_service::{CompletionKind, LanguageService};
use wasm_bindgen::prelude::*;
#[cfg(any(target_arch = "wasm32", test))]
mod value;
#[cfg(target_arch = "wasm32")]
mod web_environment;
#[cfg(target_arch = "wasm32")]
use web_environment::JsEnvironment;
/// Evaluates one standalone Decodal source with no host globals or imports.
#[wasm_bindgen]
pub fn evaluate(source: &str) -> String {
encode_result(evaluate_inner(source))
}
/// A browser-facing language service configured entirely by its JavaScript host.
///
/// The host owns globals, import loading, and import completion. This keeps
/// filesystem, network, and virtual-project policy outside the WASM package.
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(js_name = DecodalLanguageService)]
pub struct WebLanguageService {
environment: JsEnvironment,
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(js_class = DecodalLanguageService)]
impl WebLanguageService {
#[wasm_bindgen(constructor)]
pub fn new(options: JsValue) -> Result<WebLanguageService, JsValue> {
Ok(Self {
environment: JsEnvironment::from_options(options)?,
})
}
/// Evaluates a source using the injected globals and import loader.
pub fn evaluate(&self, key: &str, name: &str, source: &str) -> String {
encode_result(evaluate_with_environment(
&self.environment,
key,
name,
source,
))
}
/// Completes a source using the same injected environment as evaluation.
///
/// Positions and returned ranges are UTF-16 offsets, matching browser
/// editors and the Language Server Protocol.
pub fn complete(&self, key: &str, source: &str, position: usize, explicit: bool) -> String {
encode_completion(&self.environment, key, source, position, explicit)
}
}
fn encode_result(result: Result<String, String>) -> String {
match result {
Ok(output) => serde_json::json!({ "ok": true, "output": output }).to_string(),
Err(error) => serde_json::json!({ "ok": false, "error": error }).to_string(),
}
}
fn evaluate_inner(source: &str) -> Result<String, String> {
let mut engine = Engine::new(EmptyLoader);
let module = match engine.add_root_source("playground", "playground", source) {
Ok(module) => module,
Err(error) => return Err(format_diagnostic_with_root(&error, "playground")),
};
let value = match engine.eval_module(module) {
Ok(value) => value,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
let data = match engine.materialize(&value) {
Ok(data) => data,
Err(error) => return Err(engine.format_diagnostic(&error)),
};
Ok(format_data(&data, 0))
}
#[cfg(target_arch = "wasm32")]
fn evaluate_with_environment(
environment: &JsEnvironment,
key: &str,
name: &str,
source: &str,
) -> Result<String, String> {
let mut engine = environment
.create_engine()
.map_err(|diagnostic| diagnostic.message)?;
let module = match engine.add_root_source(key, name, source) {
Ok(module) => module,
Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)),
};
let value = match engine.eval_module(module) {
Ok(value) => value,
Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)),
};
let data = match engine.materialize(&value) {
Ok(data) => data,
Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)),
};
Ok(format_data(&data, 0))
}
#[cfg(target_arch = "wasm32")]
fn encode_completion(
environment: &JsEnvironment,
key: &str,
source: &str,
position: usize,
explicit: bool,
) -> String {
let byte_position = utf16_offset_to_byte(source, position);
let service = LanguageService::new(environment);
let completion = match service.complete(key, source, byte_position, explicit) {
Ok(completion) => completion,
Err(diagnostic) => {
return serde_json::json!({
"ok": false,
"error": diagnostic.message,
})
.to_string();
}
};
let completion = completion.map(|completion| {
let from = byte_offset_to_utf16(source, completion.from);
let options = completion
.items
.into_iter()
.map(|item| {
serde_json::json!({
"label": item.label,
"kind": completion_kind_name(item.kind),
"detail": item.detail,
"priority": item.priority,
})
})
.collect::<Vec<_>>();
serde_json::json!({ "from": from, "options": options })
});
serde_json::json!({ "ok": true, "completion": completion }).to_string()
}
#[cfg(target_arch = "wasm32")]
fn completion_kind_name(kind: CompletionKind) -> &'static str {
match kind {
CompletionKind::Keyword => "keyword",
CompletionKind::Constant => "constant",
CompletionKind::Type => "type",
CompletionKind::Variable => "variable",
CompletionKind::Namespace => "namespace",
CompletionKind::Property => "property",
CompletionKind::File => "file",
}
}
#[cfg(any(target_arch = "wasm32", test))]
fn utf16_offset_to_byte(source: &str, offset: usize) -> usize {
let mut utf16 = 0usize;
for (byte, ch) in source.char_indices() {
if utf16 >= offset {
return byte;
}
let next = utf16 + ch.len_utf16();
if next > offset {
return byte;
}
utf16 = next;
}
source.len()
}
#[cfg(any(target_arch = "wasm32", test))]
fn byte_offset_to_utf16(source: &str, mut offset: usize) -> usize {
offset = offset.min(source.len());
while !source.is_char_boundary(offset) {
offset = offset.saturating_sub(1);
}
source[..offset].encode_utf16().count()
}
fn format_diagnostic_with_root(diagnostic: &decodal::Diagnostic, root_name: &str) -> String {
format_diagnostic_with(diagnostic, |source| {
(source == SourceId(0)).then_some(root_name)
})
}
fn format_data(data: &Data, indent: usize) -> String {
match data {
Data::String(value) => json_string(value),
Data::Int(value) => value.to_string(),
Data::Float(value) => value.to_string(),
Data::Bool(value) => value.to_string(),
Data::Array(items) => {
if items.is_empty() {
return String::from("[]");
}
let mut out = String::from("[\n");
for (index, item) in items.iter().enumerate() {
out.push_str(&" ".repeat(indent + 2));
out.push_str(&format_data(item, indent + 2));
if index + 1 != items.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(&" ".repeat(indent));
out.push(']');
out
}
Data::Object(fields) => {
if fields.is_empty() {
return String::from("{}");
}
let mut out = String::from("{\n");
for (index, field) in fields.iter().enumerate() {
out.push_str(&" ".repeat(indent + 2));
out.push_str(&json_string(&field.name));
out.push_str(": ");
out.push_str(&format_data(&field.value, indent + 2));
if index + 1 != fields.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(&" ".repeat(indent));
out.push('}');
out
}
}
}
fn json_string(value: &str) -> String {
serde_json::to_string(value).expect("strings are always JSON-serializable")
}
#[cfg(test)]
mod tests {
use super::{byte_offset_to_utf16, evaluate_inner, utf16_offset_to_byte};
#[test]
fn evaluates_standalone_sources() {
assert_eq!(
evaluate_inner("value = 1;").unwrap(),
"{\n \"value\": 1\n}"
);
}
#[test]
fn converts_web_utf16_offsets() {
let source = "a😀β";
assert_eq!(utf16_offset_to_byte(source, 0), 0);
assert_eq!(utf16_offset_to_byte(source, 1), 1);
assert_eq!(utf16_offset_to_byte(source, 3), 5);
assert_eq!(byte_offset_to_utf16(source, 5), 3);
assert_eq!(byte_offset_to_utf16(source, source.len()), 4);
}
}