summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2021-06-09 19:07:50 -0400
committerGitHub <noreply@github.com>2021-06-09 19:07:50 -0400
commit67690b78bda16b90c0b9b79e369eb67eb3a9822a (patch)
treeaa82fcdaab2a50c0c5e823ef62f1808a7b399c3d
parente75ffab0c8a21ecb0827bb906905cd0315c1b5a7 (diff)
refactor(repl): Extract out structs for internal REPL code (#10915)
* Extract out ReplEditor. * Extract out ReplSession. * Move PRELUDE declaration up.
-rw-r--r--cli/tools/repl.rs491
1 files changed, 268 insertions, 223 deletions
diff --git a/cli/tools/repl.rs b/cli/tools/repl.rs
index 47f46c361..e48aa3685 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<Value>)>,
response_rx: Receiver<Result<Value, AnyError>>,
}
-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<Value>)>,
- response_tx: &Sender<Result<Value, AnyError>>,
- editor: Arc<Mutex<Editor<Helper>>>,
-) -> Result<String, ReadlineError> {
- let mut line =
- tokio::task::spawn_blocking(move || editor.lock().unwrap().readline("> "));
+#[derive(Clone)]
+struct ReplEditor {
+ inner: Arc<Mutex<Editor<EditorHelper>>>,
+ 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<String, ReadlineError> {
+ 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?;
+struct ReplSession {
+ worker: MainWorker,
+ session: LocalInspectorSession,
+ pub context_id: u64,
+}
- Ok(())
+impl ReplSession {
+ pub async fn initialize(mut worker: MainWorker) -> Result<Self, AnyError> {
+ let mut session = worker.create_inspector_session().await;
+
+ 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<bool, AnyError> {
+ 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<Value>,
+ ) -> Result<Value, AnyError> {
+ 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<Value, AnyError> {
+ // 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<String, AnyError> {
+ // 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<Value, AnyError> {
+ self
+ .post_message_with_event_loop(
+ "Runtime.evaluate",
+ Some(json!({
+ "expression": expression,
+ "contextId": self.context_id,
+ "replMode": true,
+ })),
+ )
+ .await
+ }
}
-pub async fn is_closing(
- worker: &mut MainWorker,
- session: &mut LocalInspectorSession,
- context_id: u64,
-) -> Result<bool, AnyError> {
- 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();
-
- Ok(closed)
+async fn read_line_and_poll(
+ repl_session: &mut ReplSession,
+ message_rx: &Receiver<(String, Option<Value>)>,
+ response_tx: &Sender<Result<Value, AnyError>>,
+ editor: ReplEditor,
+) -> Result<String, ReadlineError> {
+ 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(())
}