chisel/
solidity_helper.rs

1//! This module contains the `SolidityHelper`, a [rustyline::Helper] implementation for
2//! usage in Chisel. It was originally ported from [soli](https://github.com/jpopesculian/soli/blob/master/src/main.rs).
3
4use crate::{
5    dispatcher::PROMPT_ARROW,
6    prelude::{COMMAND_LEADER, ChiselCommand, PROMPT_ARROW_STR},
7};
8use rustyline::{
9    Helper,
10    completion::Completer,
11    highlight::{CmdKind, Highlighter},
12    hint::Hinter,
13    validate::{ValidationContext, ValidationResult, Validator},
14};
15use solar::parse::{
16    Cursor, Lexer,
17    interface::Session,
18    lexer::token::{RawLiteralKind, RawTokenKind},
19    token::Token,
20};
21use std::{borrow::Cow, cell::RefCell, fmt, ops::Range, rc::Rc};
22use yansi::{Color, Style};
23
24/// The maximum length of an ANSI prefix + suffix characters using [SolidityHelper].
25///
26/// * 5 - prefix:
27///   * 2 - start: `\x1B[`
28///   * 2 - fg: `3<fg_code>`
29///   * 1 - end: `m`
30/// * 4 - suffix: `\x1B[0m`
31const MAX_ANSI_LEN: usize = 9;
32
33/// A rustyline helper for Solidity code.
34#[derive(Clone)]
35pub struct SolidityHelper {
36    inner: Rc<RefCell<Inner>>,
37}
38
39struct Inner {
40    errored: bool,
41
42    do_paint: bool,
43    sess: Session,
44}
45
46impl Default for SolidityHelper {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl fmt::Debug for SolidityHelper {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        let this = self.inner.borrow_mut();
55        f.debug_struct("SolidityHelper")
56            .field("errored", &this.errored)
57            .field("do_paint", &this.do_paint)
58            .finish_non_exhaustive()
59    }
60}
61
62impl SolidityHelper {
63    /// Create a new SolidityHelper.
64    pub fn new() -> Self {
65        Self {
66            inner: Rc::new(RefCell::new(Inner {
67                errored: false,
68                do_paint: yansi::is_enabled(),
69                sess: Session::builder().with_silent_emitter(None).build(),
70            })),
71        }
72    }
73
74    /// Returns whether the helper is in an errored state.
75    pub fn errored(&self) -> bool {
76        self.inner.borrow().errored
77    }
78
79    /// Set the errored field.
80    pub fn set_errored(&mut self, errored: bool) -> &mut Self {
81        self.inner.borrow_mut().errored = errored;
82        self
83    }
84
85    /// Highlights a Solidity source string.
86    pub fn highlight<'a>(&self, input: &'a str) -> Cow<'a, str> {
87        if !self.do_paint() {
88            return Cow::Borrowed(input);
89        }
90
91        // Highlight commands separately
92        if let Some(full_cmd) = input.strip_prefix(COMMAND_LEADER) {
93            let (cmd, rest) = match input.split_once(' ') {
94                Some((cmd, rest)) => (cmd, Some(rest)),
95                None => (input, None),
96            };
97            let cmd = cmd.strip_prefix(COMMAND_LEADER).unwrap_or(cmd);
98
99            let mut out = String::with_capacity(input.len() + MAX_ANSI_LEN);
100
101            // cmd
102            out.push(COMMAND_LEADER);
103            let cmd_res = ChiselCommand::parse(full_cmd);
104            let style = (if cmd_res.is_ok() { Color::Green } else { Color::Red }).foreground();
105            Self::paint_unchecked(cmd, style, &mut out);
106
107            // rest
108            match rest {
109                Some(rest) if !rest.is_empty() => {
110                    out.push(' ');
111                    out.push_str(rest);
112                }
113                _ => {}
114            }
115
116            Cow::Owned(out)
117        } else {
118            let mut out = String::with_capacity(input.len() * 2);
119            self.with_contiguous_styles(input, |style, range| {
120                Self::paint_unchecked(&input[range], style, &mut out);
121            });
122            Cow::Owned(out)
123        }
124    }
125
126    /// Returns a list of styles and the ranges they should be applied to.
127    ///
128    /// Covers the entire source string, including any whitespace.
129    fn with_contiguous_styles(&self, input: &str, mut f: impl FnMut(Style, Range<usize>)) {
130        self.enter(|sess| {
131            let len = input.len();
132            let mut index = 0;
133            for token in Lexer::new(sess, input) {
134                let range = token.span.lo().to_usize()..token.span.hi().to_usize();
135                let style = token_style(&token);
136                if index < range.start {
137                    f(Style::default(), index..range.start);
138                }
139                index = range.end;
140                f(style, range);
141            }
142            if index < len {
143                f(Style::default(), index..len);
144            }
145        });
146    }
147
148    /// Validate that a source snippet is closed (i.e., all braces and parenthesis are matched).
149    fn validate_closed(&self, input: &str) -> ValidationResult {
150        use RawLiteralKind::*;
151        use RawTokenKind::*;
152        let mut stack = vec![];
153        for token in Cursor::new(input) {
154            match token.kind {
155                OpenDelim(delim) => stack.push(delim),
156                CloseDelim(delim) => match (stack.pop(), delim) {
157                    (Some(open), close) if open == close => {}
158                    (Some(wanted), _) => {
159                        let wanted = wanted.to_open_str();
160                        return ValidationResult::Invalid(Some(format!(
161                            "Mismatched brackets: `{wanted}` is not properly closed"
162                        )));
163                    }
164                    (None, c) => {
165                        let c = c.to_close_str();
166                        return ValidationResult::Invalid(Some(format!(
167                            "Mismatched brackets: `{c}` is unpaired"
168                        )));
169                    }
170                },
171
172                Literal { kind: Str { terminated, .. } } => {
173                    if !terminated {
174                        return ValidationResult::Incomplete;
175                    }
176                }
177
178                BlockComment { terminated, .. } => {
179                    if !terminated {
180                        return ValidationResult::Incomplete;
181                    }
182                }
183
184                _ => {}
185            }
186        }
187
188        // There are open brackets that are not properly closed.
189        if !stack.is_empty() {
190            return ValidationResult::Incomplete;
191        }
192
193        ValidationResult::Valid(None)
194    }
195
196    /// Formats `input` with `style` into `out`, without checking `style.wrapping` or
197    /// `self.do_paint`.
198    fn paint_unchecked(string: &str, style: Style, out: &mut String) {
199        if style == Style::default() {
200            out.push_str(string);
201        } else {
202            let _ = style.fmt_prefix(out);
203            out.push_str(string);
204            let _ = style.fmt_suffix(out);
205        }
206    }
207
208    fn paint_unchecked_owned(string: &str, style: Style) -> String {
209        let mut out = String::with_capacity(MAX_ANSI_LEN + string.len());
210        Self::paint_unchecked(string, style, &mut out);
211        out
212    }
213
214    /// Returns whether to color the output.
215    fn do_paint(&self) -> bool {
216        self.inner.borrow().do_paint
217    }
218
219    /// Enters the session.
220    fn enter(&self, f: impl FnOnce(&Session)) {
221        let this = self.inner.borrow();
222        this.sess.enter_sequential(|| f(&this.sess));
223    }
224}
225
226impl Highlighter for SolidityHelper {
227    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
228        self.highlight(line)
229    }
230
231    fn highlight_char(&self, line: &str, pos: usize, _kind: CmdKind) -> bool {
232        pos == line.len()
233    }
234
235    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
236        &'s self,
237        prompt: &'p str,
238        _default: bool,
239    ) -> Cow<'b, str> {
240        if !self.do_paint() {
241            return Cow::Borrowed(prompt);
242        }
243
244        let mut out = prompt.to_string();
245
246        // `^(\(ID: .*?\) )? ➜ `
247        if prompt.starts_with("(ID: ") {
248            let id_end = prompt.find(')').unwrap();
249            let id_span = 5..id_end;
250            let id = &prompt[id_span.clone()];
251            out.replace_range(
252                id_span,
253                &Self::paint_unchecked_owned(id, Color::Yellow.foreground()),
254            );
255            out.replace_range(1..=2, &Self::paint_unchecked_owned("ID", Color::Cyan.foreground()));
256        }
257
258        if let Some(i) = out.find(PROMPT_ARROW) {
259            let style =
260                if self.errored() { Color::Red.foreground() } else { Color::Green.foreground() };
261            out.replace_range(i..=i + 2, &Self::paint_unchecked_owned(PROMPT_ARROW_STR, style));
262        }
263
264        Cow::Owned(out)
265    }
266}
267
268impl Validator for SolidityHelper {
269    fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> {
270        Ok(self.validate_closed(ctx.input()))
271    }
272}
273
274impl Completer for SolidityHelper {
275    type Candidate = String;
276}
277
278impl Hinter for SolidityHelper {
279    type Hint = String;
280}
281
282impl Helper for SolidityHelper {}
283
284#[expect(non_upper_case_globals)]
285#[deny(unreachable_patterns)]
286fn token_style(token: &Token) -> Style {
287    use solar::parse::{
288        interface::kw::*,
289        token::{TokenKind::*, TokenLitKind::*},
290    };
291
292    match token.kind {
293        Literal(Str | HexStr | UnicodeStr, _) => Color::Green.foreground(),
294        Literal(..) => Color::Yellow.foreground(),
295
296        Ident(
297            Memory | Storage | Calldata | Public | Private | Internal | External | Constant | Pure
298            | View | Payable | Anonymous | Indexed | Abstract | Virtual | Override | Modifier
299            | Immutable | Unchecked,
300        ) => Color::Cyan.foreground(),
301
302        Ident(s) if s.is_elementary_type() => Color::Blue.foreground(),
303        Ident(Mapping) => Color::Blue.foreground(),
304
305        Ident(s) if s.is_used_keyword() || s.is_yul_keyword() => Color::Magenta.foreground(),
306        Arrow | FatArrow => Color::Magenta.foreground(),
307
308        Comment(..) => Color::Primary.dim(),
309
310        _ => Color::Primary.foreground(),
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn validate() {
320        let helper = SolidityHelper::new();
321        let dbg_r = |r: ValidationResult| match r {
322            ValidationResult::Incomplete => "Incomplete".to_string(),
323            ValidationResult::Invalid(inner) => format!("Invalid({inner:?})"),
324            ValidationResult::Valid(inner) => format!("Valid({inner:?})"),
325            _ => "Unknown result".to_string(),
326        };
327        let valid = |input: &str| {
328            let r = helper.validate_closed(input);
329            assert!(matches!(r, ValidationResult::Valid(None)), "{input:?}: {}", dbg_r(r))
330        };
331        let incomplete = |input: &str| {
332            let r = helper.validate_closed(input);
333            assert!(matches!(r, ValidationResult::Incomplete), "{input:?}: {}", dbg_r(r))
334        };
335        let invalid = |input: &str| {
336            let r = helper.validate_closed(input);
337            assert!(matches!(r, ValidationResult::Invalid(Some(_))), "{input:?}: {}", dbg_r(r))
338        };
339
340        valid("1");
341        valid("1 + 2");
342
343        valid("()");
344        valid("{}");
345        valid("[]");
346
347        incomplete("(");
348        incomplete("((");
349        incomplete("[");
350        incomplete("{");
351        incomplete("({");
352        valid("({})");
353
354        invalid(")");
355        invalid("]");
356        invalid("}");
357        invalid("(}");
358        invalid("(})");
359        invalid("[}");
360        invalid("[}]");
361
362        incomplete("\"");
363        incomplete("\'");
364        valid("\"\"");
365        valid("\'\'");
366
367        incomplete("/*");
368        incomplete("/*/*");
369        valid("/* */");
370        valid("/* /* */");
371        valid("/* /* */ */");
372    }
373}