deno 2.9.0

Provides the deno executable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
// Copyright 2018-2026 the Deno authors. MIT license.

use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering::Relaxed;

use deno_ast::swc::parser::error::SyntaxError;
use deno_ast::swc::parser::token::BinOpToken;
use deno_ast::swc::parser::token::Token;
use deno_ast::swc::parser::token::Word;
use deno_ast::view::AssignOp;
use deno_core::anyhow::Context as _;
use deno_core::error::AnyError;
use deno_core::parking_lot::Mutex;
use deno_core::serde_json;
use rustyline::Cmd;
use rustyline::CompletionType;
use rustyline::ConditionalEventHandler;
use rustyline::Config;
use rustyline::Context;
use rustyline::Editor;
use rustyline::Event;
use rustyline::EventContext;
use rustyline::EventHandler;
use rustyline::KeyCode;
use rustyline::KeyEvent;
use rustyline::Modifiers;
use rustyline::RepeatCount;
use rustyline::completion::Completer;
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::validate::ValidationContext;
use rustyline::validate::ValidationResult;
use rustyline::validate::Validator;
use rustyline_derive::Helper;
use rustyline_derive::Hinter;

use super::channel::RustylineSyncMessageSender;
use crate::cdp;
use crate::colors;

// Provides helpers to the editor like validation for multi-line edits, completion candidates for
// tab completion.
#[derive(Helper, Hinter)]
pub struct EditorHelper {
  pub context_id: u64,
  pub sync_sender: RustylineSyncMessageSender,
}

impl EditorHelper {
  pub fn get_global_lexical_scope_names(&self) -> Vec<String> {
    let evaluate_response = self
      .sync_sender
      .post_message(
        "Runtime.globalLexicalScopeNames",
        Some(cdp::GlobalLexicalScopeNamesArgs {
          execution_context_id: Some(self.context_id),
        }),
      )
      .unwrap();
    let evaluate_response: cdp::GlobalLexicalScopeNamesResponse =
      serde_json::from_value(evaluate_response).unwrap();
    evaluate_response.names
  }

  pub fn get_expression_property_names(&self, expr: &str) -> Vec<String> {
    // try to get the properties from the expression
    if let Some(properties) = self.get_object_expr_properties(expr) {
      return properties;
    }

    // otherwise fall back to the prototype
    let expr_type = self.get_expression_type(expr);
    let object_expr = match expr_type.as_deref() {
      // possibilities: https://chromedevtools.github.io/devtools-protocol/v8/Runtime/#type-RemoteObject
      Some("object") => "Object.prototype",
      Some("function") => "Function.prototype",
      Some("string") => "String.prototype",
      Some("boolean") => "Boolean.prototype",
      Some("bigint") => "BigInt.prototype",
      Some("number") => "Number.prototype",
      _ => return Vec::new(), // undefined, symbol, and unhandled
    };

    self
      .get_object_expr_properties(object_expr)
      .unwrap_or_default()
  }

  fn get_expression_type(&self, expr: &str) -> Option<String> {
    self.evaluate_expression(expr).map(|res| res.result.kind)
  }

  fn get_object_expr_properties(
    &self,
    object_expr: &str,
  ) -> Option<Vec<String>> {
    let evaluate_result = self.evaluate_expression(object_expr)?;
    let object_id = evaluate_result.result.object_id?;

    let get_properties_response = self
      .sync_sender
      .post_message(
        "Runtime.getProperties",
        Some(cdp::GetPropertiesArgs {
          object_id,
          own_properties: None,
          accessor_properties_only: None,
          generate_preview: None,
          non_indexed_properties_only: Some(true),
        }),
      )
      .ok()?;
    let get_properties_response: cdp::GetPropertiesResponse =
      serde_json::from_value(get_properties_response).ok()?;
    Some(
      get_properties_response
        .result
        .into_iter()
        .map(|prop| prop.name)
        .collect(),
    )
  }

  fn evaluate_expression(&self, expr: &str) -> Option<cdp::EvaluateResponse> {
    // First evaluate with side-effect protection so that pressing <Tab> never
    // runs code with observable side effects.
    let response = self.post_evaluate(expr, true)?;

    // Some globals are exposed through lazily-loaded getters whose bytecode
    // calls native ops the first time they are accessed (e.g. `navigator.gpu`
    // loads the webgpu extension). V8's side-effect analysis is static, so it
    // flags such getters as side-effecting and aborts the evaluation, leaving
    // completion with nothing to inspect. Completion expressions are always
    // plain member-access chains -- the expression extractor treats `(`, `[`,
    // etc. as word boundaries -- so the only possible side effect here is a
    // property getter. Retry once without the protection so completion still
    // works for these. See https://github.com/denoland/deno/issues/24917
    let response = if response
      .exception_details
      .as_ref()
      .is_some_and(is_side_effect_error)
    {
      self.post_evaluate(expr, false)?
    } else {
      response
    };

    if response.exception_details.is_some() {
      None
    } else {
      Some(response)
    }
  }

  fn post_evaluate(
    &self,
    expr: &str,
    throw_on_side_effect: bool,
  ) -> Option<cdp::EvaluateResponse> {
    let evaluate_response = self
      .sync_sender
      .post_message(
        "Runtime.evaluate",
        Some(cdp::EvaluateArgs {
          expression: expr.to_string(),
          object_group: None,
          include_command_line_api: None,
          silent: None,
          context_id: Some(self.context_id),
          return_by_value: None,
          generate_preview: None,
          user_gesture: None,
          await_promise: None,
          throw_on_side_effect: Some(throw_on_side_effect),
          timeout: Some(200),
          disable_breaks: None,
          repl_mode: None,
          allow_unsafe_eval_blocked_by_csp: None,
          unique_context_id: None,
        }),
      )
      .ok()?;
    serde_json::from_value(evaluate_response).ok()
  }
}

/// V8 aborts evaluations that look side-effecting (under `throwOnSideEffect`)
/// with `EvalError: Possible side-effect in debug-evaluate`.
fn is_side_effect_error(details: &cdp::ExceptionDetails) -> bool {
  details.text.contains("side-effect")
    || details
      .exception
      .as_ref()
      .and_then(|ex| ex.description.as_deref())
      .is_some_and(|desc| desc.contains("side-effect"))
}

fn is_word_boundary(c: char) -> bool {
  if matches!(c, '.' | '_' | '$') {
    false
  } else {
    char::is_ascii_whitespace(&c) || char::is_ascii_punctuation(&c)
  }
}

fn get_expr_from_line_at_pos(line: &str, cursor_pos: usize) -> &str {
  let start = line[..cursor_pos].rfind(is_word_boundary).unwrap_or(0);
  let word = &line[start..cursor_pos];
  word.strip_prefix(is_word_boundary).unwrap_or(word)
}

impl Completer for EditorHelper {
  type Candidate = String;

  fn complete(
    &self,
    line: &str,
    pos: usize,
    _ctx: &Context<'_>,
  ) -> Result<(usize, Vec<String>), ReadlineError> {
    let expr = get_expr_from_line_at_pos(line, pos);

    // check if the expression is in the form `obj.prop`
    if let Some(index) = expr.rfind('.') {
      let sub_expr = &expr[..index];
      let prop_name = &expr[index + 1..];
      let candidates = self
        .get_expression_property_names(sub_expr)
        .into_iter()
        .filter(|n| !n.starts_with("Symbol(") && n.starts_with(prop_name))
        .collect();

      Ok((pos - prop_name.len(), candidates))
    } else {
      // combine results of declarations and globalThis properties
      let mut candidates = self
        .get_expression_property_names("globalThis")
        .into_iter()
        .chain(self.get_global_lexical_scope_names())
        .filter(|n| n.starts_with(expr))
        .collect::<Vec<_>>();

      // sort and remove duplicates
      candidates.sort();
      candidates.dedup(); // make sure to sort first

      Ok((pos - expr.len(), candidates))
    }
  }
}

impl Validator for EditorHelper {
  fn validate(
    &self,
    ctx: &mut ValidationContext,
  ) -> Result<ValidationResult, ReadlineError> {
    Ok(validate(ctx.input()))
  }
}

fn validate(input: &str) -> ValidationResult {
  let line_info = text_lines::TextLines::new(input);
  let mut stack: Vec<Token> = Vec::new();
  let mut in_template = false;
  let mut div_token_count_on_current_line = 0;
  let mut last_line_index = 0;
  let mut queued_validation_error = None;
  // Whether the last (non-comment) token was a `.`, which means a member
  // access is still missing its property and more input should be read.
  let mut ends_with_dot = false;
  let tokens = deno_ast::lex(input, deno_ast::MediaType::TypeScript)
    .into_iter()
    .filter_map(|item| match item.inner {
      deno_ast::TokenOrComment::Token(token) => Some((token, item.range)),
      deno_ast::TokenOrComment::Comment { .. } => None,
    });

  for (token, range) in tokens {
    let current_line_index = line_info.line_index(range.start);
    if current_line_index != last_line_index {
      div_token_count_on_current_line = 0;
      last_line_index = current_line_index;

      if let Some(error) = queued_validation_error {
        return error;
      }
    }
    ends_with_dot = matches!(token, Token::Dot);
    match token {
      Token::BinOp(BinOpToken::Div) | Token::AssignOp(AssignOp::DivAssign) => {
        // it's too complicated to write code to detect regular expression literals
        // which are no longer tokenized, so if a `/` or `/=` happens twice on the same
        // line, then we bail
        div_token_count_on_current_line += 1;
        if div_token_count_on_current_line >= 2 {
          return ValidationResult::Valid(None);
        }
      }
      Token::BackQuote => in_template = !in_template,
      Token::LParen | Token::LBracket | Token::LBrace | Token::DollarLBrace => {
        stack.push(token)
      }
      Token::RParen | Token::RBracket | Token::RBrace => {
        match (stack.pop(), token) {
          (Some(Token::LParen), Token::RParen)
          | (Some(Token::LBracket), Token::RBracket)
          | (Some(Token::LBrace), Token::RBrace)
          | (Some(Token::DollarLBrace), Token::RBrace) => {}
          (Some(left), _) => {
            // queue up a validation error to surface once we've finished examining the current line
            queued_validation_error = Some(ValidationResult::Invalid(Some(
              format!("Mismatched pairs: {left:?} is not properly closed"),
            )));
          }
          (None, _) => {
            // While technically invalid when unpaired, it should be V8's task to output error instead.
            // Thus marked as valid with no info.
            return ValidationResult::Valid(None);
          }
        }
      }
      Token::Error(error) => {
        match error.kind() {
          // If there is unterminated template, it continues to read input.
          SyntaxError::UnterminatedTpl => {}
          _ => {
            // If it failed parsing, it should be V8's task to output error instead.
            // Thus marked as valid with no info.
            return ValidationResult::Valid(None);
          }
        }
      }
      _ => {}
    }
  }

  if let Some(error) = queued_validation_error {
    error
  } else if !stack.is_empty() || in_template || ends_with_dot {
    // A trailing `.` means the user broke a method chain across lines (e.g.
    // `foo.\n  bar()`), so keep reading input instead of evaluating the
    // incomplete member access. See https://github.com/denoland/deno/issues/16335
    ValidationResult::Incomplete
  } else {
    ValidationResult::Valid(None)
  }
}

/// Scan a regex literal starting at the opening `/` located at `start` and
/// return the byte index just past its closing `/` and flags, or `None` if the
/// text isn't a well-formed single-line regex literal.
///
/// `deno_ast::lex` runs the lexer without the parser, so it never reads regex
/// literals itself — they come back as a division operator followed by however
/// the rest of the pattern happens to tokenize (often a stray string literal
/// when the pattern contains quotes). We rescan them by hand to highlight them
/// correctly.
fn find_regex_literal_end(line: &str, start: usize) -> Option<usize> {
  let mut chars = line[start + 1..].char_indices();
  let mut in_class = false;
  let mut end = None;
  while let Some((rel, c)) = chars.next() {
    match c {
      // Skip the escaped character.
      '\\' => {
        chars.next();
      }
      // Regex literals can't span multiple lines.
      '\n' | '\r' => return None,
      '[' => in_class = true,
      ']' => in_class = false,
      '/' if !in_class => {
        // `start + 1` skips the opening `/`, `+ rel` is the offset of the
        // closing `/` within the remainder, `+ 1` moves just past it.
        end = Some(start + 1 + rel + 1);
        break;
      }
      _ => {}
    }
  }
  let mut end = end?;
  // Consume trailing flags (e.g. the `gi` in `/foo/gi`).
  while let Some(c) = line[end..].chars().next() {
    if c.is_ascii_alphabetic() {
      end += c.len_utf8();
    } else {
      break;
    }
  }
  Some(end)
}

/// Apply ANSI color codes to a single line of REPL input for syntax
/// highlighting.
fn highlight_line(line: &str) -> String {
  let mut out_line = String::from(line);
  let items = deno_ast::lex(line, deno_ast::MediaType::TypeScript);
  // Whether a `/` at the current position would begin a regex literal rather
  // than act as a division operator. This mirrors the "before expression"
  // heuristic the parser uses to disambiguate the two. It starts `true` because
  // a `/` at the very beginning of the input always starts a regex.
  //
  // Like a standalone SWC lexer, this is knowingly imperfect after `)` and
  // `++`/`--`: `if (x) /re/.test(y)` renders as division and `x++ /2/` looks
  // like a regex. Both are harmless for highlighting and not worth tracking
  // full expression context to fix.
  let mut regex_allowed = true;
  // Byte offset (in `line`) up to which the input has already been consumed by
  // a detected regex literal; tokens starting before it should be skipped.
  let mut skip_until = 0;

  for (index, item) in items.iter().enumerate() {
    let start = item.range.start;
    let end = item.range.end;
    if start < skip_until {
      // This token is part of a regex literal that was already colored.
      continue;
    }
    // Adding color adds more bytes to the string,
    // so an offset is needed to stop spans falling out of sync.
    let offset = out_line.len() - line.len();

    // A regex literal is reported by the lexer as a division operator (`/` or
    // `/=`); detect and color the whole literal in one piece.
    if regex_allowed
      && let deno_ast::TokenOrComment::Token(
        Token::BinOp(BinOpToken::Div) | Token::AssignOp(AssignOp::DivAssign),
      ) = &item.inner
      && let Some(regex_end) = find_regex_literal_end(line, start)
    {
      out_line.replace_range(
        start + offset..regex_end + offset,
        &colors::red(&line[start..regex_end]).to_string(),
      );
      skip_until = regex_end;
      regex_allowed = false;
      continue;
    }

    out_line.replace_range(
      start + offset..end + offset,
      &match &item.inner {
        deno_ast::TokenOrComment::Token(token) => {
          let colored = match token {
            Token::Str { .. } | Token::Template { .. } | Token::BackQuote => {
              colors::green(&line[start..end]).to_string()
            }
            // Dead arm today: `deno_ast::lex` runs without parser context so it
            // never emits `Token::Regex` (regexes arrive as a `Div`/`DivAssign`
            // handled above). Kept in case the lexer ever gains that context.
            Token::Regex(_, _) => colors::red(&line[start..end]).to_string(),
            Token::Num { .. } | Token::BigInt { .. } => {
              colors::yellow(&line[start..end]).to_string()
            }
            Token::Word(word) => match word {
              Word::True | Word::False | Word::Null => {
                colors::yellow(&line[start..end]).to_string()
              }
              Word::Keyword(_) => colors::cyan(&line[start..end]).to_string(),
              Word::Ident(ident) => {
                match ident.as_ref() {
                  "undefined" => colors::gray(&line[start..end]).to_string(),
                  "Infinity" | "NaN" => {
                    colors::yellow(&line[start..end]).to_string()
                  }
                  "async" | "of" => colors::cyan(&line[start..end]).to_string(),
                  _ => {
                    let next = items.get(index + 1).map(|item| &item.inner);
                    if matches!(
                      next,
                      Some(deno_ast::TokenOrComment::Token(Token::LParen))
                    ) {
                      // We're looking for something that looks like a function
                      // We use a simple heuristic: 'ident' followed by 'LParen'
                      colors::intense_blue(&line[start..end]).to_string()
                    } else if ident.as_ref() == "from"
                      && matches!(
                        next,
                        Some(deno_ast::TokenOrComment::Token(
                          Token::Str { .. }
                        ))
                      )
                    {
                      // When ident 'from' is followed by a string literal, highlight it
                      // E.g. "export * from 'something'" or "import a from 'something'"
                      colors::cyan(&line[start..end]).to_string()
                    } else {
                      line[start..end].to_string()
                    }
                  }
                }
              }
            },
            _ => line[start..end].to_string(),
          };
          // Track whether the next `/` should be treated as a regex literal.
          regex_allowed = token.kind().before_expr();
          colored
        }
        deno_ast::TokenOrComment::Comment { .. } => {
          // Comments don't affect whether a following `/` starts a regex.
          colors::gray(&line[start..end]).to_string()
        }
      },
    );
  }

  out_line
}

impl Highlighter for EditorHelper {
  fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
    hint.into()
  }

  fn highlight_candidate<'c>(
    &self,
    candidate: &'c str,
    completion: rustyline::CompletionType,
  ) -> Cow<'c, str> {
    if completion == CompletionType::List {
      candidate.into()
    } else {
      self.highlight(candidate, 0)
    }
  }

  fn highlight_char(
    &self,
    line: &str,
    _: usize,
    _: rustyline::highlight::CmdKind,
  ) -> bool {
    !line.is_empty()
  }

  fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> {
    highlight_line(line).into()
  }
}

#[derive(Clone)]
pub struct ReplEditor {
  inner: Arc<Mutex<Editor<EditorHelper, rustyline::history::FileHistory>>>,
  history_file_path: Option<PathBuf>,
  errored_on_history_save: Arc<AtomicBool>,
  should_exit_on_interrupt: Arc<AtomicBool>,
}

impl ReplEditor {
  pub fn new(
    helper: EditorHelper,
    history_file_path: Option<PathBuf>,
  ) -> Result<Self, AnyError> {
    let editor_config = Config::builder()
      .completion_type(CompletionType::List)
      .build();

    let mut editor =
      Editor::with_config(editor_config).expect("Failed to create editor.");
    editor.set_helper(Some(helper));
    if let Some(history_file_path) = &history_file_path {
      editor.load_history(history_file_path).unwrap_or(());
    }
    editor.bind_sequence(
      KeyEvent(KeyCode::Char('s'), Modifiers::CTRL),
      EventHandler::Simple(Cmd::Newline),
    );
    editor.bind_sequence(
      KeyEvent(KeyCode::Tab, Modifiers::NONE),
      EventHandler::Conditional(Box::new(TabEventHandler)),
    );
    let should_exit_on_interrupt = Arc::new(AtomicBool::new(false));
    editor.bind_sequence(
      KeyEvent(KeyCode::Char('r'), Modifiers::CTRL),
      EventHandler::Conditional(Box::new(ReverseSearchHistoryEventHandler {
        should_exit_on_interrupt: should_exit_on_interrupt.clone(),
      })),
    );

    if let Some(history_file_path) = &history_file_path {
      let history_file_dir = history_file_path.parent().unwrap();
      std::fs::create_dir_all(history_file_dir).with_context(|| {
        format!(
          "Unable to create directory for the history file: {}",
          history_file_dir.display()
        )
      })?;
    }

    Ok(ReplEditor {
      inner: Arc::new(Mutex::new(editor)),
      history_file_path,
      errored_on_history_save: Arc::new(AtomicBool::new(false)),
      should_exit_on_interrupt,
    })
  }

  pub fn readline(&self) -> Result<String, ReadlineError> {
    self.inner.lock().readline("> ")
  }

  pub fn update_history(&self, entry: String) {
    let _ = self.inner.lock().add_history_entry(entry);
    if let Some(history_file_path) = &self.history_file_path
      && let Err(e) = self.inner.lock().append_history(history_file_path)
    {
      if self.errored_on_history_save.load(Relaxed) {
        return;
      }

      self.errored_on_history_save.store(true, Relaxed);
      log::warn!("Unable to save history file: {}", e);
    }
  }

  pub fn should_exit_on_interrupt(&self) -> bool {
    self.should_exit_on_interrupt.load(Relaxed)
  }

  pub fn set_should_exit_on_interrupt(&self, yes: bool) {
    self.should_exit_on_interrupt.store(yes, Relaxed);
  }
}

/// Command to reverse search history , same as rustyline default C-R but that resets repl should_exit flag to false
struct ReverseSearchHistoryEventHandler {
  should_exit_on_interrupt: Arc<AtomicBool>,
}
impl ConditionalEventHandler for ReverseSearchHistoryEventHandler {
  fn handle(
    &self,
    _: &Event,
    _: RepeatCount,
    _: bool,
    _: &EventContext,
  ) -> Option<Cmd> {
    self.should_exit_on_interrupt.store(false, Relaxed);
    Some(Cmd::ReverseSearchHistory)
  }
}

/// A custom tab key event handler
/// It uses a heuristic to determine if the user is requesting completion or if they want to insert an actual tab
/// The heuristic goes like this:
///   - If the last character before the cursor is whitespace, the user wants to insert a tab
///   - Else the user is requesting completion
struct TabEventHandler;
impl ConditionalEventHandler for TabEventHandler {
  fn handle(
    &self,
    evt: &Event,
    n: RepeatCount,
    _: bool,
    ctx: &EventContext,
  ) -> Option<Cmd> {
    debug_assert_eq!(
      *evt,
      Event::from(KeyEvent(KeyCode::Tab, Modifiers::NONE))
    );
    if ctx.line().is_empty()
      || ctx.line()[..ctx.pos()]
        .chars()
        .next_back()
        .filter(|c| c.is_whitespace())
        .is_some()
    {
      if cfg!(target_os = "windows") {
        // Inserting a tab is broken in windows with rustyline
        // use 4 spaces as a workaround for now
        Some(Cmd::Insert(n, "    ".into()))
      } else {
        Some(Cmd::Insert(n, "\t".into()))
      }
    } else {
      None // default complete
    }
  }
}

#[cfg(test)]
mod test {
  use rustyline::validate::ValidationResult;

  use super::validate;

  #[test]
  fn validate_only_one_forward_slash_per_line() {
    let code = r#"function test(arr){
if( arr.length <= 1) return arr.map(a => a / 2)
let left = test( arr.slice( 0 , arr.length/2 ) )"#;
    assert!(matches!(validate(code), ValidationResult::Incomplete));
  }

  #[test]
  fn validate_regex_looking_code() {
    let code = r#"/testing/;"#;
    assert!(matches!(validate(code), ValidationResult::Valid(_)));
  }

  #[test]
  fn find_regex_literal_end_basic() {
    use super::find_regex_literal_end;
    // Simple regex with flags.
    assert_eq!(find_regex_literal_end("/foo/gi", 0), Some(7));
    // Closing `/` inside a character class doesn't end the regex.
    assert_eq!(find_regex_literal_end("/[a/b]/", 0), Some(7));
    // Escaped `/` doesn't end the regex.
    assert_eq!(find_regex_literal_end(r"/a\/b/", 0), Some(6));
    // The regex from denoland/deno#20751 (quotes in the pattern).
    let re = r##"/https?:\/\/deno.land\/(?:std\@?[^\'\"]*|x\/[^\/\"\']*?\@?[^\'\"]*)/"##;
    assert_eq!(find_regex_literal_end(re, 0), Some(re.len()));
    // Not a regex: no closing slash.
    assert_eq!(find_regex_literal_end("/foo", 0), None);
    // Not a regex: spans a newline.
    assert_eq!(find_regex_literal_end("/foo\n/", 0), None);
    // `/=`-style regex (lexed as a `DivAssign` whose pattern starts with `=`).
    assert_eq!(find_regex_literal_end("/=foo/", 0), Some(6));
  }

  #[test]
  fn highlight_regex_literal_is_one_unit() {
    use super::highlight_line;

    // The SGR sequence `colors::red` wraps its content in. Asserting against
    // these fixed bytes — rather than re-deriving them via `colors::red`, which
    // reads the same global color flag `highlight_line` does — keeps the
    // negative assertions reliable even if another test toggles that global:
    // with color off, `colors::red("/ b /")` would return the bare string,
    // which *is* contained in the division output and would flake.
    fn red(s: &str) -> String {
      format!("\u{1b}[31m{s}\u{1b}[0m")
    }

    // `highlight_line` only emits color codes when the global color flag is on.
    // Save and restore it so we neither assume its prior value nor leak our
    // change to tests that run afterwards.
    let prev = deno_terminal::colors::use_color();
    deno_terminal::colors::set_use_color(true);

    // A regex containing quotes used to leak a green string literal mid-regex;
    // now the whole literal is colored red as a single unit.
    let re =
      r##"/https?:\/\/deno.land\/(?:std\@?[^\'\"]*|x\/[^\'\"]*?\@?[^\'\"]*)/"##;
    assert!(highlight_line(re).contains(&red(re)));

    // A bare regex at the start of a line is highlighted.
    assert!(highlight_line("/foo/g").contains(&red("/foo/g")));

    // A regex in expression position (after `=`) is highlighted.
    assert!(highlight_line("const re = /foo/;").contains(&red("/foo/")));

    // A regex in keyword position (after `return`) is highlighted.
    assert!(highlight_line("return /foo/").contains(&red("/foo/")));

    // Division is NOT treated as a regex.
    assert!(!highlight_line("a / b / c").contains(&red("/ b /")));

    // A `/=` following a value stays a division-assignment, not a regex.
    assert!(!highlight_line("x /= 2").contains(&red("/= 2")));

    deno_terminal::colors::set_use_color(prev);
  }

  #[test]
  fn validate_trailing_dot_is_incomplete() {
    // A method chain broken right after the `.` should keep reading input
    // instead of being evaluated as an incomplete member access.
    assert!(matches!(
      validate("[1, 2, 3]."),
      ValidationResult::Incomplete
    ));
    assert!(matches!(
      validate("(await Promise.all([])).\n"),
      ValidationResult::Incomplete
    ));
    // Once the property name is supplied the input is complete again.
    assert!(matches!(
      validate("[1, 2, 3].\nmap(x => x)"),
      ValidationResult::Valid(_)
    ));
  }

  #[test]
  fn validate_dot_inside_number_is_valid() {
    // `5.` is a valid number literal, not a trailing member access.
    assert!(matches!(validate("5."), ValidationResult::Valid(_)));
  }
}