0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-10-29 08:58:01 -04:00

refactor(repl): Extract out structs for internal REPL code (#10915)

* Extract out ReplEditor.
* Extract out ReplSession.
* Move PRELUDE declaration up.
This commit is contained in:
David Sherret 2021-06-09 19:07:50 -04:00 committed by GitHub
parent e75ffab0c8
commit 67690b78bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -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?;
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<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();
impl ReplSession {
pub async fn initialize(mut worker: MainWorker) -> Result<Self, AnyError> {
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<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
}
}
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(())
}