1
//! Outbound SLYNK event constructors. Each returns the wire string (already
2
//! `sexp::write`-encoded, ready to frame). Shapes are pinned to the captured
3
//! transcript (`doc/editor/slynk-protocol-transcript.org`).
4

            
5
use super::sexp::{self, Sexp};
6

            
7
68
fn sym(s: &str) -> Sexp {
8
68
    Sexp::Symbol(s.to_string())
9
68
}
10

            
11
53
fn list(items: Vec<Sexp>) -> Sexp {
12
53
    Sexp::List(items)
13
53
}
14

            
15
/// `(:return (:ok VALUE) ID)` — successful rex reply.
16
7
pub fn return_ok(value: Sexp, id: i64) -> String {
17
7
    sexp::write(&list(vec![
18
7
        sym(":return"),
19
7
        list(vec![sym(":ok"), value]),
20
7
        Sexp::Int(id),
21
7
    ]))
22
7
}
23

            
24
/// `(:return (:abort "reason") ID)` — rex we don't implement.
25
3
pub fn return_abort(reason: &str, id: i64) -> String {
26
3
    sexp::write(&list(vec![
27
3
        sym(":return"),
28
3
        list(vec![sym(":abort"), Sexp::Str(reason.to_string())]),
29
3
        Sexp::Int(id),
30
3
    ]))
31
3
}
32

            
33
/// The `slynk:connection-info` reply value (the `&key` plist SLY destructures).
34
/// `pid` is this process; the package name/prompt drive the mREPL prompt text.
35
/// `:version` is cosmetic here — `sly-protocol-version` is nil in the target
36
/// build, so no version check fires (see the transcript doc).
37
1
pub fn connection_info(pid: i64) -> Sexp {
38
1
    list(vec![
39
1
        sym(":pid"),
40
1
        Sexp::Int(pid),
41
1
        sym(":style"),
42
1
        sym("nil"),
43
1
        sym(":lisp-implementation"),
44
1
        list(vec![
45
1
            sym(":type"),
46
1
            Sexp::Str("nomiscript".into()),
47
1
            sym(":name"),
48
1
            Sexp::Str("nms".into()),
49
1
            sym(":version"),
50
1
            Sexp::Str(env!("CARGO_PKG_VERSION").into()),
51
        ]),
52
1
        sym(":machine"),
53
1
        list(vec![
54
1
            sym(":instance"),
55
1
            Sexp::Str(String::new()),
56
1
            sym(":type"),
57
1
            Sexp::Str(String::new()),
58
1
            sym(":version"),
59
1
            Sexp::Str(String::new()),
60
        ]),
61
1
        sym(":features"),
62
1
        sym("nil"),
63
1
        sym(":modules"),
64
1
        sym("nil"),
65
1
        sym(":version"),
66
1
        Sexp::Str("2.30".into()),
67
1
        sym(":encoding"),
68
1
        list(vec![
69
1
            sym(":coding-systems"),
70
1
            list(vec![Sexp::Str("utf-8-unix".into())]),
71
        ]),
72
1
        sym(":package"),
73
1
        list(vec![
74
1
            sym(":name"),
75
1
            Sexp::Str(PACKAGE.into()),
76
1
            sym(":prompt"),
77
1
            Sexp::Str(PACKAGE.into()),
78
        ]),
79
    ])
80
1
}
81

            
82
/// The single pseudo-package name used everywhere (connection-info + prompts).
83
pub const PACKAGE: &str = "nomiscript";
84

            
85
/// The `slynk:slynk-require` reply: the list of "loaded" modules. Advertise
86
/// ONLY what we actually implement — `slynk/mrepl` (the listener). SLY enables
87
/// behaviour per advertised module, so advertising e.g. `slynk/arglists` /
88
/// `slynk/indentation` while we `:abort` their rex is a contract mismatch; we
89
/// leave them out and SLY simply doesn't request those features.
90
pub fn require_modules() -> Sexp {
91
    list(vec![Sexp::Str("slynk/mrepl".into())])
92
}
93

            
94
/// `slynk-mrepl:create-mrepl` reply value: `(REMOTE-CHANNEL-ID THREAD-ID)`.
95
1
pub fn create_mrepl_ok(remote_channel: i64, thread: i64) -> Sexp {
96
1
    list(vec![Sexp::Int(remote_channel), Sexp::Int(thread)])
97
1
}
98

            
99
/// `(:channel-send CHAN (:prompt PACKAGE NICKNAME 0))`.
100
4
pub fn prompt(channel: i64) -> String {
101
4
    channel_send(
102
4
        channel,
103
4
        list(vec![
104
4
            sym(":prompt"),
105
4
            Sexp::Str(PACKAGE.into()),
106
4
            Sexp::Str(PACKAGE.into()),
107
4
            Sexp::Int(0),
108
        ]),
109
    )
110
4
}
111

            
112
/// `(:channel-send CHAN (:write-string "TEXT"))`.
113
2
pub fn write_string(channel: i64, text: &str) -> String {
114
2
    channel_send(
115
2
        channel,
116
2
        list(vec![sym(":write-string"), Sexp::Str(text.to_string())]),
117
    )
118
2
}
119

            
120
/// `(:channel-send CHAN (:write-values (("PRINTED" nil nil))))`.
121
3
pub fn write_values(channel: i64, printed: &str) -> String {
122
3
    channel_send(
123
3
        channel,
124
3
        list(vec![
125
3
            sym(":write-values"),
126
3
            list(vec![list(vec![
127
3
                Sexp::Str(printed.to_string()),
128
3
                sym("nil"),
129
3
                sym("nil"),
130
            ])]),
131
        ]),
132
    )
133
3
}
134

            
135
/// `(:channel-send CHAN (:evaluation-aborted "CONDITION"))`.
136
1
pub fn evaluation_aborted(channel: i64, condition: &str) -> String {
137
1
    channel_send(
138
1
        channel,
139
1
        list(vec![
140
1
            sym(":evaluation-aborted"),
141
1
            Sexp::Str(condition.to_string()),
142
        ]),
143
    )
144
1
}
145

            
146
/// `(:ping THREAD TAG)` → answered with `(:emacs-pong THREAD TAG)`.
147
pub fn emacs_pong(thread: Sexp, tag: Sexp) -> String {
148
    sexp::write(&list(vec![sym(":emacs-pong"), thread, tag]))
149
}
150

            
151
10
fn channel_send(channel: i64, message: Sexp) -> String {
152
10
    sexp::write(&list(vec![
153
10
        sym(":channel-send"),
154
10
        Sexp::Int(channel),
155
10
        message,
156
10
    ]))
157
10
}
158

            
159
#[cfg(test)]
160
mod tests {
161
    use super::*;
162

            
163
    #[test]
164
1
    fn return_ok_matches_transcript() {
165
1
        assert_eq!(return_ok(sym("nil"), 2), "(:return (:ok nil) 2)");
166
1
    }
167

            
168
    #[test]
169
1
    fn abort_matches_transcript() {
170
1
        assert_eq!(
171
1
            return_abort("unimplemented", 7),
172
            "(:return (:abort \"unimplemented\") 7)"
173
        );
174
1
    }
175

            
176
    #[test]
177
1
    fn prompt_matches_transcript() {
178
1
        assert_eq!(
179
1
            prompt(1),
180
            "(:channel-send 1 (:prompt \"nomiscript\" \"nomiscript\" 0))"
181
        );
182
1
    }
183

            
184
    #[test]
185
1
    fn write_values_matches_transcript() {
186
1
        assert_eq!(
187
1
            write_values(1, "3"),
188
            "(:channel-send 1 (:write-values ((\"3\" nil nil))))"
189
        );
190
1
    }
191

            
192
    #[test]
193
1
    fn write_string_escapes() {
194
1
        assert_eq!(
195
1
            write_string(1, "a\"b"),
196
            "(:channel-send 1 (:write-string \"a\\\"b\"))"
197
        );
198
1
    }
199

            
200
    #[test]
201
1
    fn create_mrepl_reply_is_a_pair() {
202
1
        assert_eq!(super::sexp::write(&create_mrepl_ok(1, 1)), "(1 1)");
203
1
    }
204

            
205
    #[test]
206
1
    fn connection_info_has_mrepl_package() {
207
1
        let s = super::sexp::write(&connection_info(42));
208
1
        assert!(s.contains(":package"));
209
1
        assert!(s.contains("nomiscript"));
210
1
        assert!(s.contains(":pid 42"));
211
1
    }
212
}