Skip to main content

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();
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, .. } } if !terminated => {
173                    return ValidationResult::Incomplete;
174                }
175
176                BlockComment { terminated, .. } if !terminated => {
177                    return ValidationResult::Incomplete;
178                }
179
180                _ => {}
181            }
182        }
183
184        // There are open brackets that are not properly closed.
185        if !stack.is_empty() {
186            return ValidationResult::Incomplete;
187        }
188
189        ValidationResult::Valid(None)
190    }
191
192    /// Formats `input` with `style` into `out`, without checking `style.wrapping` or
193    /// `self.do_paint`.
194    fn paint_unchecked(string: &str, style: Style, out: &mut String) {
195        if style == Style::default() {
196            out.push_str(string);
197        } else {
198            let _ = style.fmt_prefix(out);
199            out.push_str(string);
200            let _ = style.fmt_suffix(out);
201        }
202    }
203
204    fn paint_unchecked_owned(string: &str, style: Style) -> String {
205        let mut out = String::with_capacity(MAX_ANSI_LEN + string.len());
206        Self::paint_unchecked(string, style, &mut out);
207        out
208    }
209
210    /// Returns whether to color the output.
211    fn do_paint(&self) -> bool {
212        self.inner.borrow().do_paint
213    }
214
215    /// Enters the session.
216    fn enter(&self, f: impl FnOnce(&Session)) {
217        let this = self.inner.borrow();
218        this.sess.enter_sequential(|| f(&this.sess));
219    }
220}
221
222impl Highlighter for SolidityHelper {
223    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
224        self.highlight(line)
225    }
226
227    fn highlight_char(&self, line: &str, pos: usize, _kind: CmdKind) -> bool {
228        pos == line.len()
229    }
230
231    fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
232        &'s self,
233        prompt: &'p str,
234        _default: bool,
235    ) -> Cow<'b, str> {
236        if !self.do_paint() {
237            return Cow::Borrowed(prompt);
238        }
239
240        let mut out = prompt.to_string();
241
242        // `^(\(ID: .*?\) )? ➜ `
243        if prompt.starts_with("(ID: ") {
244            let id_end = prompt.find(')').unwrap();
245            let id_span = 5..id_end;
246            let id = &prompt[id_span.clone()];
247            out.replace_range(
248                id_span,
249                &Self::paint_unchecked_owned(id, Color::Yellow.foreground()),
250            );
251            out.replace_range(1..=2, &Self::paint_unchecked_owned("ID", Color::Cyan.foreground()));
252        }
253
254        if let Some(i) = out.find(PROMPT_ARROW) {
255            let style =
256                if self.errored() { Color::Red.foreground() } else { Color::Green.foreground() };
257            out.replace_range(i..=i + 2, &Self::paint_unchecked_owned(PROMPT_ARROW_STR, style));
258        }
259
260        Cow::Owned(out)
261    }
262}
263
264impl Validator for SolidityHelper {
265    fn validate(&self, ctx: &mut ValidationContext<'_>) -> rustyline::Result<ValidationResult> {
266        Ok(self.validate_closed(ctx.input()))
267    }
268}
269
270impl Completer for SolidityHelper {
271    type Candidate = String;
272}
273
274impl Hinter for SolidityHelper {
275    type Hint = String;
276}
277
278impl Helper for SolidityHelper {}
279
280#[expect(non_upper_case_globals)]
281#[deny(unreachable_patterns)]
282fn token_style(token: &Token) -> Style {
283    use solar::parse::{
284        interface::kw::*,
285        token::{TokenKind::*, TokenLitKind::*},
286    };
287
288    match token.kind {
289        Literal(Str | HexStr | UnicodeStr, _) => Color::Green.foreground(),
290        Literal(..) => Color::Yellow.foreground(),
291
292        Ident(
293            Memory | Storage | Calldata | Public | Private | Internal | External | Constant | Pure
294            | View | Payable | Anonymous | Indexed | Abstract | Virtual | Override | Modifier
295            | Immutable | Unchecked,
296        ) => Color::Cyan.foreground(),
297
298        Ident(s) if s.is_elementary_type() => Color::Blue.foreground(),
299        Ident(Mapping) => Color::Blue.foreground(),
300
301        Ident(s) if s.is_used_keyword() || s.is_yul_keyword() => Color::Magenta.foreground(),
302        Arrow | FatArrow => Color::Magenta.foreground(),
303
304        Comment(..) => Color::Primary.dim(),
305
306        _ => Color::Primary.foreground(),
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn validate() {
316        let helper = SolidityHelper::new();
317        let dbg_r = |r: ValidationResult| match r {
318            ValidationResult::Incomplete => "Incomplete".to_string(),
319            ValidationResult::Invalid(inner) => format!("Invalid({inner:?})"),
320            ValidationResult::Valid(inner) => format!("Valid({inner:?})"),
321            _ => "Unknown result".to_string(),
322        };
323        let valid = |input: &str| {
324            let r = helper.validate_closed(input);
325            assert!(matches!(r, ValidationResult::Valid(None)), "{input:?}: {}", dbg_r(r))
326        };
327        let incomplete = |input: &str| {
328            let r = helper.validate_closed(input);
329            assert!(matches!(r, ValidationResult::Incomplete), "{input:?}: {}", dbg_r(r))
330        };
331        let invalid = |input: &str| {
332            let r = helper.validate_closed(input);
333            assert!(matches!(r, ValidationResult::Invalid(Some(_))), "{input:?}: {}", dbg_r(r))
334        };
335
336        valid("1");
337        valid("1 + 2");
338
339        valid("()");
340        valid("{}");
341        valid("[]");
342
343        incomplete("(");
344        incomplete("((");
345        incomplete("[");
346        incomplete("{");
347        incomplete("({");
348        valid("({})");
349
350        invalid(")");
351        invalid("]");
352        invalid("}");
353        invalid("(}");
354        invalid("(})");
355        invalid("[}");
356        invalid("[}]");
357
358        incomplete("\"");
359        incomplete("\'");
360        valid("\"\"");
361        valid("\'\'");
362
363        incomplete("/*");
364        incomplete("/*/*");
365        valid("/* */");
366        valid("/* /* */");
367        valid("/* /* */ */");
368    }
369}