264 lines
8.5 KiB
Rust
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);
|
|
}
|
|
}
|