Lines
95.52 %
Functions
39.13 %
Branches
100 %
//! Outbound SLYNK event constructors. Each returns the wire string (already
//! `sexp::write`-encoded, ready to frame). Shapes are pinned to the captured
//! transcript (`doc/editor/slynk-protocol-transcript.org`).
use super::sexp::{self, Sexp};
fn sym(s: &str) -> Sexp {
Sexp::Symbol(s.to_string())
}
fn list(items: Vec<Sexp>) -> Sexp {
Sexp::List(items)
/// `(:return (:ok VALUE) ID)` — successful rex reply.
pub fn return_ok(value: Sexp, id: i64) -> String {
sexp::write(&list(vec![
sym(":return"),
list(vec![sym(":ok"), value]),
Sexp::Int(id),
]))
/// `(:return (:abort "reason") ID)` — rex we don't implement.
pub fn return_abort(reason: &str, id: i64) -> String {
list(vec![sym(":abort"), Sexp::Str(reason.to_string())]),
/// The `slynk:connection-info` reply value (the `&key` plist SLY destructures).
/// `pid` is this process; the package name/prompt drive the mREPL prompt text.
/// `:version` is cosmetic here — `sly-protocol-version` is nil in the target
/// build, so no version check fires (see the transcript doc).
pub fn connection_info(pid: i64) -> Sexp {
list(vec![
sym(":pid"),
Sexp::Int(pid),
sym(":style"),
sym("nil"),
sym(":lisp-implementation"),
sym(":type"),
Sexp::Str("nomiscript".into()),
sym(":name"),
Sexp::Str("nms".into()),
sym(":version"),
Sexp::Str(env!("CARGO_PKG_VERSION").into()),
]),
sym(":machine"),
sym(":instance"),
Sexp::Str(String::new()),
sym(":features"),
sym(":modules"),
Sexp::Str("2.30".into()),
sym(":encoding"),
sym(":coding-systems"),
list(vec![Sexp::Str("utf-8-unix".into())]),
sym(":package"),
Sexp::Str(PACKAGE.into()),
sym(":prompt"),
])
/// The single pseudo-package name used everywhere (connection-info + prompts).
pub const PACKAGE: &str = "nomiscript";
/// The `slynk:slynk-require` reply: the list of "loaded" modules. Advertise
/// ONLY what we actually implement — `slynk/mrepl` (the listener). SLY enables
/// behaviour per advertised module, so advertising e.g. `slynk/arglists` /
/// `slynk/indentation` while we `:abort` their rex is a contract mismatch; we
/// leave them out and SLY simply doesn't request those features.
pub fn require_modules() -> Sexp {
list(vec![Sexp::Str("slynk/mrepl".into())])
/// `slynk-mrepl:create-mrepl` reply value: `(REMOTE-CHANNEL-ID THREAD-ID)`.
pub fn create_mrepl_ok(remote_channel: i64, thread: i64) -> Sexp {
list(vec![Sexp::Int(remote_channel), Sexp::Int(thread)])
/// `(:channel-send CHAN (:prompt PACKAGE NICKNAME 0))`.
pub fn prompt(channel: i64) -> String {
channel_send(
channel,
Sexp::Int(0),
)
/// `(:channel-send CHAN (:write-string "TEXT"))`.
pub fn write_string(channel: i64, text: &str) -> String {
list(vec![sym(":write-string"), Sexp::Str(text.to_string())]),
/// `(:channel-send CHAN (:write-values (("PRINTED" nil nil))))`.
pub fn write_values(channel: i64, printed: &str) -> String {
sym(":write-values"),
list(vec![list(vec![
Sexp::Str(printed.to_string()),
])]),
/// `(:channel-send CHAN (:evaluation-aborted "CONDITION"))`.
pub fn evaluation_aborted(channel: i64, condition: &str) -> String {
sym(":evaluation-aborted"),
Sexp::Str(condition.to_string()),
/// `(:ping THREAD TAG)` → answered with `(:emacs-pong THREAD TAG)`.
pub fn emacs_pong(thread: Sexp, tag: Sexp) -> String {
sexp::write(&list(vec![sym(":emacs-pong"), thread, tag]))
fn channel_send(channel: i64, message: Sexp) -> String {
sym(":channel-send"),
Sexp::Int(channel),
message,
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn return_ok_matches_transcript() {
assert_eq!(return_ok(sym("nil"), 2), "(:return (:ok nil) 2)");
fn abort_matches_transcript() {
assert_eq!(
return_abort("unimplemented", 7),
"(:return (:abort \"unimplemented\") 7)"
);
fn prompt_matches_transcript() {
prompt(1),
"(:channel-send 1 (:prompt \"nomiscript\" \"nomiscript\" 0))"
fn write_values_matches_transcript() {
write_values(1, "3"),
"(:channel-send 1 (:write-values ((\"3\" nil nil))))"
fn write_string_escapes() {
write_string(1, "a\"b"),
"(:channel-send 1 (:write-string \"a\\\"b\"))"
fn create_mrepl_reply_is_a_pair() {
assert_eq!(super::sexp::write(&create_mrepl_ok(1, 1)), "(1 1)");
fn connection_info_has_mrepl_package() {
let s = super::sexp::write(&connection_info(42));
assert!(s.contains(":package"));
assert!(s.contains("nomiscript"));
assert!(s.contains(":pid 42"));