1use 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
24const MAX_ANSI_LEN: usize = 9;
32
33#[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 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 pub fn errored(&self) -> bool {
76 self.inner.borrow().errored
77 }
78
79 pub fn set_errored(&mut self, errored: bool) -> &mut Self {
81 self.inner.borrow_mut().errored = errored;
82 self
83 }
84
85 pub fn highlight<'a>(&self, input: &'a str) -> Cow<'a, str> {
87 if !self.do_paint() {
88 return Cow::Borrowed(input);
89 }
90
91 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 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 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 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 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 if !stack.is_empty() {
186 return ValidationResult::Incomplete;
187 }
188
189 ValidationResult::Valid(None)
190 }
191
192 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 fn do_paint(&self) -> bool {
212 self.inner.borrow().do_paint
213 }
214
215 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 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}