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:
parent
e75ffab0c8
commit
67690b78bd
1 changed files with 268 additions and 223 deletions
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue