1use std::borrow::Cow::{self, Borrowed, Owned};
4
5use rustyline::completion::FilenameCompleter;
6use rustyline::highlight::Highlighter;
7use rustyline::hint::HistoryHinter;
8use rustyline::validate::{ValidationContext, ValidationResult, Validator};
9use rustyline_derive::{Completer, Helper, Hinter};
10
11use crate::colours::*;
12
13pub const KEYWORDS: &[&str] = &[
15 "lambda", "let", "if", "lift", "run", "quote",
16 "null?", "number?", "symbol?", "pair?", "car", "cdr", "cons", "eq?",
17 "lift-ref", "log", "code?", "read", "print",
18 "evalms", "trans", "catch",
19];
20
21#[derive(Helper, Completer, Hinter)]
25pub struct NarjuHelper {
26 #[rustyline(Completer)]
28 pub completer: FilenameCompleter,
29 #[rustyline(Hinter)]
31 pub hinter: HistoryHinter,
32}
33
34impl Highlighter for NarjuHelper {
35 fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
36 Owned(format!("{DIM}{hint}{RESET}"))
37 }
38
39 fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
40 &'s self,
41 prompt: &'p str,
42 _default: bool,
43 ) -> Cow<'b, str> {
44 Borrowed(prompt)
45 }
46
47 fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
48 Owned(highlight_sexp(line, Some(pos)))
49 }
50
51 fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
52 true
53 }
54}
55
56pub fn validate_brackets(input: &str) -> rustyline::Result<ValidationResult> {
60 let mut depth: i32 = 0;
61 let mut in_comment = false;
62 for ch in input.chars() {
63 if in_comment {
64 if ch == '\n' {
65 in_comment = false;
66 }
67 continue;
68 }
69 match ch {
70 ';' => in_comment = true,
71 '(' => depth += 1,
72 ')' => depth -= 1,
73 _ => {}
74 }
75 }
76 if depth > 0 {
77 Ok(ValidationResult::Incomplete)
78 } else if depth < 0 {
79 Ok(ValidationResult::Invalid(Some(format!(
80 " {WARN}unmatched ')'{RESET}"
81 ))))
82 } else {
83 Ok(ValidationResult::Valid(None))
84 }
85}
86
87impl Validator for NarjuHelper {
89 fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result<ValidationResult> {
90 validate_brackets(ctx.input())
91 }
92}
93
94fn bracket_pairs(src: &str) -> std::collections::HashMap<usize, usize> {
107 let mut pairs = std::collections::HashMap::new();
108 let mut stack: Vec<usize> = Vec::new();
109 let mut in_comment = false;
110 for (byte_off, ch) in src.char_indices() {
111 if in_comment {
112 if ch == '\n' {
113 in_comment = false;
114 }
115 continue;
116 }
117 match ch {
118 ';' => in_comment = true,
119 '(' => stack.push(byte_off),
120 ')' => {
121 if let Some(open) = stack.pop() {
122 pairs.insert(open, byte_off);
123 pairs.insert(byte_off, open);
124 }
125 }
126 _ => {}
127 }
128 }
129 pairs
130}
131
132pub struct NarjuHelperLite;
146
147impl rustyline::completion::Completer for NarjuHelperLite {
148 type Candidate = String;
149 fn complete(
150 &self,
151 _line: &str,
152 _pos: usize,
153 _ctx: &rustyline::Context<'_>,
154 ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
155 Ok((0, Vec::new()))
156 }
157}
158
159impl rustyline::hint::Hinter for NarjuHelperLite {
160 type Hint = String;
161 fn hint(&self, _line: &str, _pos: usize, _ctx: &rustyline::Context<'_>) -> Option<String> {
162 None
163 }
164}
165
166impl Highlighter for NarjuHelperLite {
167 fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
168 Owned(highlight_sexp(line, Some(pos)))
169 }
170
171 fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
172 true
173 }
174
175 fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
176 &'s self,
177 prompt: &'p str,
178 _default: bool,
179 ) -> Cow<'b, str> {
180 Borrowed(prompt)
181 }
182}
183
184impl Validator for NarjuHelperLite {
185 fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result<ValidationResult> {
186 validate_brackets(ctx.input())
187 }
188}
189
190impl rustyline::Helper for NarjuHelperLite {}
191
192pub fn highlight_sexp(src: &str, cursor_pos: Option<usize>) -> String {
199 let pairs = bracket_pairs(src);
200 let flash_at: Option<(usize, usize)> = cursor_pos.and_then(|c| pairs.get(&c).map(|&m| (c, m)));
201 let is_flash = |off: usize| flash_at.is_some_and(|(a, b)| off == a || off == b);
202
203 let mut out = String::with_capacity(src.len() * 2);
204 let chars: Vec<char> = src.chars().collect();
205 let char_byte_offs: Vec<usize> = src.char_indices().map(|(b, _)| b).collect();
206 let mut i = 0;
207
208 while i < chars.len() {
209 let c = chars[i];
210 let byte_off = char_byte_offs.get(i).copied().unwrap_or(src.len());
211
212 if c == ';' {
213 out.push_str(DIM);
214 while i < chars.len() && chars[i] != '\n' {
215 out.push(chars[i]);
216 i += 1;
217 }
218 out.push_str(RESET);
219 continue;
220 }
221
222 if c == '(' || c == ')' {
223 if is_flash(byte_off) {
224 out.push_str(BOLD);
225 out.push_str(BRACK);
226 } else {
227 out.push_str(BRACK);
228 }
229 out.push(c);
230 out.push_str(RESET);
231 i += 1;
232 continue;
233 }
234
235 if c == '\'' {
236 out.push_str(BLOOM);
237 out.push(c);
238 out.push_str(RESET);
239 i += 1;
240 continue;
241 }
242
243 if c.is_ascii_digit()
244 || (c == '-' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit())
245 {
246 out.push_str(STEM);
247 out.push(c);
248 i += 1;
249 while i < chars.len() && chars[i].is_ascii_digit() {
250 out.push(chars[i]);
251 i += 1;
252 }
253 out.push_str(RESET);
254 continue;
255 }
256
257 if is_sym_char(c) {
258 let start = i;
259 while i < chars.len() && is_sym_char(chars[i]) {
260 i += 1;
261 }
262 let word: String = chars[start..i].iter().collect();
263 if KEYWORDS.contains(&word.as_str()) {
264 out.push_str(KW);
265 } else if word == "nil" || word == "#t" || word == "#f" {
266 out.push_str(BLOOM);
267 } else {
268 out.push_str(PETAL);
269 }
270 out.push_str(&word);
271 out.push_str(RESET);
272 continue;
273 }
274
275 out.push(c);
276 i += 1;
277 }
278 out
279}
280
281pub fn is_sym_char(c: char) -> bool {
286 c.is_alphanumeric()
287 || matches!(
288 c,
289 '+' | '-' | '*' | '/' | '?' | '!' | '<' | '>' | '=' | '_' | '#'
290 )
291}