diff options
author | Casper Beyer <caspervonb@pm.me> | 2020-10-02 07:14:55 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-02 01:14:55 +0200 |
commit | 4c779b5e8ca427faf24c26443a8054004827d450 (patch) | |
tree | ddb8ded427e4987f2edbb63c1be3661569e0d204 /cli/repl.rs | |
parent | 5590b97670206df957c43742f47601356982c658 (diff) |
refactor(repl): use an inspector session (#7763)
This ports the REPL over to Rust and makes use of an inspector session to run a REPL on top of any isolate which lets make full use of rustylines various things like validators and completors without having to introduce a bunch of hard to test internal ops and glue code.
An accidental but good side effect of this is that the multiple line input we previously had is now an editable multi-line input prompt that is correctly stored in the history as a single entry.
Diffstat (limited to 'cli/repl.rs')
-rw-r--r-- | cli/repl.rs | 298 |
1 files changed, 240 insertions, 58 deletions
diff --git a/cli/repl.rs b/cli/repl.rs index 7873f7d0f..57e517bd7 100644 --- a/cli/repl.rs +++ b/cli/repl.rs @@ -1,73 +1,255 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. -use crate::deno_dir::DenoDir; +use crate::global_state::GlobalState; +use crate::inspector::InspectorSession; use deno_core::error::AnyError; +use deno_core::serde_json::json; +use rustyline::error::ReadlineError; +use rustyline::validate::MatchingBracketValidator; +use rustyline::validate::ValidationContext; +use rustyline::validate::ValidationResult; +use rustyline::validate::Validator; use rustyline::Editor; -use std::fs; -use std::path::PathBuf; +use rustyline_derive::{Completer, Helper, Highlighter, Hinter}; +use std::sync::Arc; +use std::sync::Mutex; -pub struct Repl { - editor: Editor<()>, - history_file: PathBuf, +// Provides syntax specific helpers to the editor like validation for multi-line edits. +#[derive(Completer, Helper, Highlighter, Hinter)] +struct Helper { + validator: MatchingBracketValidator, } -impl Repl { - pub fn new(history_file: PathBuf) -> Self { - let mut repl = Self { - editor: Editor::<()>::new(), - history_file, - }; - - repl.load_history(); - repl +impl Validator for Helper { + fn validate( + &self, + ctx: &mut ValidationContext, + ) -> Result<ValidationResult, ReadlineError> { + self.validator.validate(ctx) } +} - fn load_history(&mut self) { - debug!("Loading REPL history: {:?}", self.history_file); - self - .editor - .load_history(&self.history_file.to_str().unwrap()) - .map_err(|e| { - debug!("Unable to load history file: {:?} {}", self.history_file, e) - }) - // ignore this error (e.g. it occurs on first load) - .unwrap_or(()) - } +pub async fn run( + global_state: &GlobalState, + mut session: Box<InspectorSession>, +) -> Result<(), AnyError> { + // Our inspector is unable to default to the default context id so we have to specify it here. + let context_id: u32 = 1; - fn save_history(&mut self) -> Result<(), AnyError> { - fs::create_dir_all(self.history_file.parent().unwrap())?; - self - .editor - .save_history(&self.history_file.to_str().unwrap()) - .map(|_| debug!("Saved REPL history to: {:?}", self.history_file)) - .map_err(|e| { - eprintln!("Unable to save REPL history: {:?} {}", self.history_file, e); - e.into() - }) - } + let history_file = global_state.dir.root.join("deno_history.txt"); - pub fn readline(&mut self, prompt: &str) -> Result<String, AnyError> { - self - .editor - .readline(&prompt) - .map(|line| { - self.editor.add_history_entry(line.clone()); - line - }) - .map_err(AnyError::from) - - // Forward error to TS side for processing - } -} + session + .post_message("Runtime.enable".to_string(), None) + .await?; + + let helper = Helper { + validator: MatchingBracketValidator::new(), + }; + + let editor = Arc::new(Mutex::new(Editor::new())); + + editor.lock().unwrap().set_helper(Some(helper)); + + editor + .lock() + .unwrap() + .load_history(history_file.to_str().unwrap()) + .unwrap_or(()); + + println!("Deno {}", crate::version::DENO); + println!("exit using ctrl+d or close()"); + + let prelude = r#" + Object.defineProperty(globalThis, "_", { + configurable: true, + get: () => Deno[Deno.internal].lastEvalResult, + set: (value) => { + Object.defineProperty(globalThis, "_", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + console.log("Last evaluation result is no longer saved to _."); + }, + }); + + Object.defineProperty(globalThis, "_error", { + configurable: true, + get: () => Deno[Deno.internal].lastThrownError, + set: (value) => { + Object.defineProperty(globalThis, "_error", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + + console.log("Last thrown error is no longer saved to _error."); + }, + }); + "#; -impl Drop for Repl { - fn drop(&mut self) { - self.save_history().unwrap(); + session + .post_message( + "Runtime.evaluate".to_string(), + Some(json!({ + "expression": prelude, + "contextId": context_id, + })), + ) + .await?; + + loop { + let editor2 = editor.clone(); + let line = tokio::task::spawn_blocking(move || { + editor2.lock().unwrap().readline("> ") + }) + .await?; + + match line { + Ok(line) => { + // It is a bit unexpected that { "foo": "bar" } is interpreted as a block + // statement rather than an object literal so we interpret it as an expression statement + // to match the behavior found in a typical prompt including browser developer tools. + let wrapped_line = if line.trim_start().starts_with('{') + && !line.trim_end().ends_with(';') + { + format!("({})", &line) + } else { + line.clone() + }; + + let evaluate_response = session + .post_message( + "Runtime.evaluate".to_string(), + Some(json!({ + "expression": format!("'use strict'; void 0;\n{}", &wrapped_line), + "contextId": context_id, + // TODO(caspervonb) set repl mode to true to enable const redeclarations and top + // level await + "replMode": false, + })), + ) + .await?; + + // If that fails, we retry it without wrapping in parens letting the error bubble up to the + // user if it is still an error. + let evaluate_response = + if evaluate_response.get("exceptionDetails").is_some() + && wrapped_line != line + { + session + .post_message( + "Runtime.evaluate".to_string(), + Some(json!({ + "expression": format!("'use strict'; void 0;\n{}", &line), + "contextId": context_id, + // TODO(caspervonb) set repl mode to true to enable const redeclarations and top + // level await + "replMode": false, + })), + ) + .await? + } else { + evaluate_response + }; + + let is_closing = session + .post_message( + "Runtime.evaluate".to_string(), + Some(json!({ + "expression": "(globalThis.closed)", + "contextId": context_id, + })), + ) + .await? + .get("result") + .unwrap() + .get("value") + .unwrap() + .as_bool() + .unwrap(); + + if is_closing { + break; + } + + let evaluate_result = evaluate_response.get("result").unwrap(); + let evaluate_exception_details = + evaluate_response.get("exceptionDetails"); + + if evaluate_exception_details.is_some() { + session + .post_message( + "Runtime.callFunctionOn".to_string(), + Some(json!({ + "executionContextId": context_id, + "functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }", + "arguments": [ + evaluate_result, + ], + }))).await?; + } else { + session + .post_message( + "Runtime.callFunctionOn".to_string(), + Some(json!({ + "executionContextId": context_id, + "functionDeclaration": "function (object) { Deno[Deno.internal].lastEvalResult = object; }", + "arguments": [ + evaluate_result, + ], + }))).await?; + } + + // TODO(caspervonb) we should investigate using previews here but to keep things + // consistent with the previous implementation we just get the preview result from + // Deno.inspectArgs. + let inspect_response = session + .post_message( + "Runtime.callFunctionOn".to_string(), + Some(json!({ + "executionContextId": context_id, + "functionDeclaration": "function (object) { return Deno[Deno.internal].inspectArgs(['%o', object]); }", + "arguments": [ + evaluate_result, + ], + }))).await?; + + let inspect_result = inspect_response.get("result").unwrap(); + + match evaluate_exception_details { + Some(_) => eprintln!( + "Uncaught {}", + inspect_result.get("value").unwrap().as_str().unwrap() + ), + None => println!( + "{}", + inspect_result.get("value").unwrap().as_str().unwrap() + ), + } + + editor.lock().unwrap().add_history_entry(line.as_str()); + } + Err(ReadlineError::Interrupted) => { + break; + } + Err(ReadlineError::Eof) => { + break; + } + Err(err) => { + println!("Error: {:?}", err); + break; + } + } } -} -pub fn history_path(dir: &DenoDir, history_file: &str) -> PathBuf { - let mut p: PathBuf = dir.root.clone(); - p.push(history_file); - p + std::fs::create_dir_all(history_file.parent().unwrap())?; + editor + .lock() + .unwrap() + .save_history(history_file.to_str().unwrap())?; + + Ok(()) } |