From 67690b78bda16b90c0b9b79e369eb67eb3a9822a Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 9 Jun 2021 19:07:50 -0400 Subject: [PATCH] refactor(repl): Extract out structs for internal REPL code (#10915) * Extract out ReplEditor. * Extract out ReplSession. * Move PRELUDE declaration up. --- cli/tools/repl.rs | 491 +++++++++++++++++++++++++--------------------- 1 file changed, 268 insertions(+), 223 deletions(-) diff --git a/cli/tools/repl.rs b/cli/tools/repl.rs index 47f46c361e..e48aa36851 100644 --- a/cli/tools/repl.rs +++ b/cli/tools/repl.rs @@ -21,6 +21,7 @@ use rustyline::Context; use rustyline::Editor; use rustyline_derive::{Helper, Hinter}; use std::borrow::Cow; +use std::path::PathBuf; use std::sync::mpsc::channel; use std::sync::mpsc::sync_channel; use std::sync::mpsc::Receiver; @@ -34,13 +35,13 @@ use tokio::pin; // Provides helpers to the editor like validation for multi-line edits, completion candidates for // tab completion. #[derive(Helper, Hinter)] -struct Helper { +struct EditorHelper { context_id: u64, message_tx: SyncSender<(String, Option)>, response_rx: Receiver>, } -impl Helper { +impl EditorHelper { fn post_message( &self, method: &str, @@ -59,7 +60,7 @@ fn is_word_boundary(c: char) -> bool { } } -impl Completer for Helper { +impl Completer for EditorHelper { type Candidate = String; fn complete( @@ -141,7 +142,7 @@ impl Completer for Helper { } } -impl Validator for Helper { +impl Validator for EditorHelper { fn validate( &self, ctx: &mut ValidationContext, @@ -189,7 +190,7 @@ impl Validator for Helper { } } -impl Highlighter for Helper { +impl Highlighter for EditorHelper { fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { hint.into() } @@ -256,44 +257,41 @@ impl Highlighter for Helper { } } -async fn read_line_and_poll( - worker: &mut MainWorker, - session: &mut LocalInspectorSession, - message_rx: &Receiver<(String, Option)>, - response_tx: &Sender>, - editor: Arc>>, -) -> Result { - let mut line = - tokio::task::spawn_blocking(move || editor.lock().unwrap().readline("> ")); +#[derive(Clone)] +struct ReplEditor { + inner: Arc>>, + history_file_path: PathBuf, +} - let mut poll_worker = true; +impl ReplEditor { + pub fn new(helper: EditorHelper, history_file_path: PathBuf) -> Self { + let mut editor = Editor::new(); + editor.set_helper(Some(helper)); + editor.load_history(&history_file_path).unwrap_or(()); - loop { - for (method, params) in message_rx.try_iter() { - let result = worker - .with_event_loop(session.post_message(&method, params).boxed_local()) - .await; - response_tx.send(result).unwrap(); + ReplEditor { + inner: Arc::new(Mutex::new(editor)), + history_file_path, } + } - // Because an inspector websocket client may choose to connect at anytime when we have an - // inspector server we need to keep polling the worker to pick up new connections. - // TODO(piscisaureus): the above comment is a red herring; figure out if/why - // the event loop isn't woken by a waker when a websocket client connects. - let timeout = tokio::time::sleep(tokio::time::Duration::from_millis(100)); - pin!(timeout); + pub fn readline(&self) -> Result { + self.inner.lock().unwrap().readline("> ") + } - tokio::select! { - result = &mut line => { - return result.unwrap(); - } - _ = worker.run_event_loop(false), if poll_worker => { - poll_worker = false; - } - _ = timeout => { - poll_worker = true - } - } + pub fn add_history_entry(&self, entry: String) { + self.inner.lock().unwrap().add_history_entry(entry); + } + + pub fn save_history(&self) -> Result<(), AnyError> { + std::fs::create_dir_all(self.history_file_path.parent().unwrap())?; + + self + .inner + .lock() + .unwrap() + .save_history(&self.history_file_path)?; + Ok(()) } } @@ -328,114 +326,251 @@ Object.defineProperty(globalThis, "_error", { }); "#; -async fn inject_prelude( - worker: &mut MainWorker, - session: &mut LocalInspectorSession, - context_id: u64, -) -> Result<(), AnyError> { - worker - .with_event_loop( - session - .post_message( - "Runtime.evaluate", - Some(json!({ - "expression": PRELUDE, - "contextId": context_id, - })), - ) - .boxed_local(), - ) - .await?; - - Ok(()) +struct ReplSession { + worker: MainWorker, + session: LocalInspectorSession, + pub context_id: u64, } -pub async fn is_closing( - worker: &mut MainWorker, - session: &mut LocalInspectorSession, - context_id: u64, -) -> Result { - let closed = worker - .with_event_loop( - session - .post_message( - "Runtime.evaluate", - Some(json!({ - "expression": "(globalThis.closed)", - "contextId": context_id, - })), - ) - .boxed_local(), - ) - .await? - .get("result") - .unwrap() - .get("value") - .unwrap() - .as_bool() - .unwrap(); +impl ReplSession { + pub async fn initialize(mut worker: MainWorker) -> Result { + let mut session = worker.create_inspector_session().await; - Ok(closed) + worker + .with_event_loop( + session.post_message("Runtime.enable", None).boxed_local(), + ) + .await?; + + // Enabling the runtime domain will always send trigger one executionContextCreated for each + // context the inspector knows about so we grab the execution context from that since + // our inspector does not support a default context (0 is an invalid context id). + let mut context_id: u64 = 0; + for notification in session.notifications() { + let method = notification.get("method").unwrap().as_str().unwrap(); + let params = notification.get("params").unwrap(); + + if method == "Runtime.executionContextCreated" { + context_id = params + .get("context") + .unwrap() + .get("id") + .unwrap() + .as_u64() + .unwrap(); + } + } + + let mut repl_session = ReplSession { + worker, + session, + context_id, + }; + + // inject prelude + repl_session.evaluate_expression(PRELUDE).await?; + + Ok(repl_session) + } + + pub async fn is_closing(&mut self) -> Result { + let closed = self + .evaluate_expression("(globalThis.closed)") + .await? + .get("result") + .unwrap() + .get("value") + .unwrap() + .as_bool() + .unwrap(); + + Ok(closed) + } + + pub async fn post_message_with_event_loop( + &mut self, + method: &str, + params: Option, + ) -> Result { + self + .worker + .with_event_loop(self.session.post_message(method, params).boxed_local()) + .await + } + + pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { + self.worker.run_event_loop(false).await + } + + pub async fn evaluate_line(&mut self, line: &str) -> Result { + // 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.to_string() + }; + + let evaluate_response = self + .evaluate_expression(&format!("'use strict'; void 0;\n{}", &wrapped_line)) + .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 + { + self + .evaluate_expression(&format!("'use strict'; void 0;\n{}", &line)) + .await? + } else { + evaluate_response + }; + + Ok(evaluate_response) + } + + pub async fn set_last_thrown_error( + &mut self, + error: &Value, + ) -> Result<(), AnyError> { + self.post_message_with_event_loop( + "Runtime.callFunctionOn", + Some(json!({ + "executionContextId": self.context_id, + "functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }", + "arguments": [ + error, + ], + })), + ).await?; + Ok(()) + } + + pub async fn set_last_eval_result( + &mut self, + evaluate_result: &Value, + ) -> Result<(), AnyError> { + self.post_message_with_event_loop( + "Runtime.callFunctionOn", + Some(json!({ + "executionContextId": self.context_id, + "functionDeclaration": "function (object) { Deno[Deno.internal].lastEvalResult = object; }", + "arguments": [ + evaluate_result, + ], + })), + ).await?; + Ok(()) + } + + pub async fn get_eval_value( + &mut self, + evaluate_result: &Value, + ) -> Result { + // 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 = self.post_message_with_event_loop( + "Runtime.callFunctionOn", + Some(json!({ + "executionContextId": self.context_id, + "functionDeclaration": "function (object) { return Deno[Deno.internal].inspectArgs(['%o', object], { colors: !Deno.noColor }); }", + "arguments": [ + evaluate_result, + ], + })), + ).await?; + + let inspect_result = inspect_response.get("result").unwrap(); + let value = inspect_result.get("value").unwrap().as_str().unwrap(); + + Ok(value.to_string()) + } + + async fn evaluate_expression( + &mut self, + expression: &str, + ) -> Result { + self + .post_message_with_event_loop( + "Runtime.evaluate", + Some(json!({ + "expression": expression, + "contextId": self.context_id, + "replMode": true, + })), + ) + .await + } +} + +async fn read_line_and_poll( + repl_session: &mut ReplSession, + message_rx: &Receiver<(String, Option)>, + response_tx: &Sender>, + editor: ReplEditor, +) -> Result { + let mut line = tokio::task::spawn_blocking(move || editor.readline()); + + let mut poll_worker = true; + + loop { + for (method, params) in message_rx.try_iter() { + let result = repl_session + .post_message_with_event_loop(&method, params) + .await; + response_tx.send(result).unwrap(); + } + + // Because an inspector websocket client may choose to connect at anytime when we have an + // inspector server we need to keep polling the worker to pick up new connections. + // TODO(piscisaureus): the above comment is a red herring; figure out if/why + // the event loop isn't woken by a waker when a websocket client connects. + let timeout = tokio::time::sleep(tokio::time::Duration::from_millis(100)); + pin!(timeout); + + tokio::select! { + result = &mut line => { + return result.unwrap(); + } + _ = repl_session.run_event_loop(), if poll_worker => { + poll_worker = false; + } + _ = timeout => { + poll_worker = true + } + } + } } pub async fn run( program_state: &ProgramState, - mut worker: MainWorker, + worker: MainWorker, ) -> Result<(), AnyError> { - let mut session = worker.create_inspector_session().await; - - let history_file = program_state.dir.root.join("deno_history.txt"); - - worker - .with_event_loop(session.post_message("Runtime.enable", None).boxed_local()) - .await?; - // Enabling the runtime domain will always send trigger one executionContextCreated for each - // context the inspector knows about so we grab the execution context from that since - // our inspector does not support a default context (0 is an invalid context id). - let mut context_id: u64 = 0; - for notification in session.notifications() { - let method = notification.get("method").unwrap().as_str().unwrap(); - let params = notification.get("params").unwrap(); - - if method == "Runtime.executionContextCreated" { - context_id = params - .get("context") - .unwrap() - .get("id") - .unwrap() - .as_u64() - .unwrap(); - } - } - + let mut repl_session = ReplSession::initialize(worker).await?; let (message_tx, message_rx) = sync_channel(1); let (response_tx, response_rx) = channel(); - let helper = Helper { - context_id, + let helper = EditorHelper { + context_id: repl_session.context_id, message_tx, response_rx, }; - 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(()); + let history_file_path = program_state.dir.root.join("deno_history.txt"); + let editor = ReplEditor::new(helper, history_file_path); println!("Deno {}", crate::version::deno()); println!("exit using ctrl+d or close()"); - inject_prelude(&mut worker, &mut session, context_id).await?; - loop { let line = read_line_and_poll( - &mut worker, - &mut session, + &mut repl_session, &message_rx, &response_tx, editor.clone(), @@ -443,56 +578,11 @@ pub async fn run( .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 = worker.with_event_loop( - session.post_message( - "Runtime.evaluate", - Some(json!({ - "expression": format!("'use strict'; void 0;\n{}", &wrapped_line), - "contextId": context_id, - "replMode": true, - })), - ).boxed_local() - ) - .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 - { - worker - .with_event_loop( - session - .post_message( - "Runtime.evaluate", - Some(json!({ - "expression": format!("'use strict'; void 0;\n{}", &line), - "contextId": context_id, - "replMode": true, - })), - ) - .boxed_local(), - ) - .await? - } else { - evaluate_response - }; + let evaluate_response = repl_session.evaluate_line(&line).await?; // We check for close and break here instead of making it a loop condition to get // consistent behavior in when the user evaluates a call to close(). - if is_closing(&mut worker, &mut session, context_id).await? { + if repl_session.is_closing().await? { break; } @@ -501,61 +591,20 @@ pub async fn run( evaluate_response.get("exceptionDetails"); if evaluate_exception_details.is_some() { - worker.with_event_loop( - session.post_message( - "Runtime.callFunctionOn", - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }", - "arguments": [ - evaluate_result, - ], - })), - ).boxed_local() - ).await?; + repl_session.set_last_thrown_error(evaluate_result).await?; } else { - worker.with_event_loop( - session.post_message( - "Runtime.callFunctionOn", - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { Deno[Deno.internal].lastEvalResult = object; }", - "arguments": [ - evaluate_result, - ], - })), - ).boxed_local() - ).await?; + repl_session.set_last_eval_result(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 = - worker.with_event_loop( - session.post_message( - "Runtime.callFunctionOn", - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { return Deno[Deno.internal].inspectArgs(['%o', object], { colors: !Deno.noColor }); }", - "arguments": [ - evaluate_result, - ], - })), - ).boxed_local() - ).await?; - - let inspect_result = inspect_response.get("result").unwrap(); - - let value = inspect_result.get("value").unwrap().as_str().unwrap(); + let value = repl_session.get_eval_value(evaluate_result).await?; let output = match evaluate_exception_details { Some(_) => format!("Uncaught {}", value), - None => value.to_string(), + None => value, }; println!("{}", output); - editor.lock().unwrap().add_history_entry(line.as_str()); + editor.add_history_entry(line); } Err(ReadlineError::Interrupted) => { println!("exit using ctrl+d or close()"); @@ -571,11 +620,7 @@ pub async fn run( } } - std::fs::create_dir_all(history_file.parent().unwrap())?; - editor - .lock() - .unwrap() - .save_history(history_file.to_str().unwrap())?; + editor.save_history()?; Ok(()) }