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_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 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, .. } } => {
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 if !stack.is_empty() {
190 return ValidationResult::Incomplete;
191 }
192
193 ValidationResult::Valid(None)
194 }
195
196 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 fn do_paint(&self) -> bool {
216 self.inner.borrow().do_paint
217 }
218
219 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 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}