diff options
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(()) } |