diff options
Diffstat (limited to 'cli/repl.rs')
-rw-r--r-- | cli/repl.rs | 612 |
1 files changed, 0 insertions, 612 deletions
diff --git a/cli/repl.rs b/cli/repl.rs deleted file mode 100644 index be85dc813..000000000 --- a/cli/repl.rs +++ /dev/null @@ -1,612 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -use crate::colors; -use crate::inspector::InspectorSession; -use crate::program_state::ProgramState; -use crate::worker::MainWorker; -use crate::worker::Worker; -use deno_core::error::AnyError; -use deno_core::serde_json::json; -use deno_core::serde_json::Value; -use regex::Captures; -use regex::Regex; -use rustyline::completion::Completer; -use rustyline::error::ReadlineError; -use rustyline::highlight::Highlighter; -use rustyline::validate::ValidationContext; -use rustyline::validate::ValidationResult; -use rustyline::validate::Validator; -use rustyline::Context; -use rustyline::Editor; -use rustyline_derive::{Helper, Hinter}; -use std::borrow::Cow; -use std::sync::mpsc::channel; -use std::sync::mpsc::sync_channel; -use std::sync::mpsc::Receiver; -use std::sync::mpsc::Sender; -use std::sync::mpsc::SyncSender; -use std::sync::Arc; -use std::sync::Mutex; - -// Provides helpers to the editor like validation for multi-line edits, completion candidates for -// tab completion. -#[derive(Helper, Hinter)] -struct Helper { - context_id: u64, - message_tx: SyncSender<(String, Option<Value>)>, - response_rx: Receiver<Result<Value, AnyError>>, - highlighter: LineHighlighter, -} - -impl Helper { - fn post_message( - &self, - method: &str, - params: Option<Value>, - ) -> Result<Value, AnyError> { - self.message_tx.send((method.to_string(), params))?; - self.response_rx.recv()? - } -} - -fn is_word_boundary(c: char) -> bool { - if c == '.' { - false - } else { - char::is_ascii_whitespace(&c) || char::is_ascii_punctuation(&c) - } -} - -impl Completer for Helper { - type Candidate = String; - - fn complete( - &self, - line: &str, - pos: usize, - _ctx: &Context<'_>, - ) -> Result<(usize, Vec<String>), ReadlineError> { - let start = line[..pos].rfind(is_word_boundary).map_or_else(|| 0, |i| i); - let end = line[pos..] - .rfind(is_word_boundary) - .map_or_else(|| pos, |i| pos + i); - - let word = &line[start..end]; - let word = word.strip_prefix(is_word_boundary).unwrap_or(word); - let word = word.strip_suffix(is_word_boundary).unwrap_or(word); - - let fallback = format!(".{}", word); - - let (prefix, suffix) = match word.rfind('.') { - Some(index) => word.split_at(index), - None => ("globalThis", fallback.as_str()), - }; - - let evaluate_response = self - .post_message( - "Runtime.evaluate", - Some(json!({ - "contextId": self.context_id, - "expression": prefix, - "throwOnSideEffect": true, - "timeout": 200, - })), - ) - .unwrap(); - - if evaluate_response.get("exceptionDetails").is_some() { - let candidates = Vec::new(); - return Ok((pos, candidates)); - } - - if let Some(result) = evaluate_response.get("result") { - if let Some(object_id) = result.get("objectId") { - let get_properties_response = self - .post_message( - "Runtime.getProperties", - Some(json!({ - "objectId": object_id, - })), - ) - .unwrap(); - - if let Some(result) = get_properties_response.get("result") { - let candidates = result - .as_array() - .unwrap() - .iter() - .map(|r| r.get("name").unwrap().as_str().unwrap().to_string()) - .filter(|r| r.starts_with(&suffix[1..])) - .collect(); - - return Ok((pos - (suffix.len() - 1), candidates)); - } - } - } - - Ok((pos, Vec::new())) - } -} - -impl Validator for Helper { - fn validate( - &self, - ctx: &mut ValidationContext, - ) -> Result<ValidationResult, ReadlineError> { - let mut stack: Vec<char> = Vec::new(); - let mut literal: Option<char> = None; - let mut escape: bool = false; - - for c in ctx.input().chars() { - if escape { - escape = false; - continue; - } - - if c == '\\' { - escape = true; - continue; - } - - if let Some(v) = literal { - if c == v { - literal = None - } - - continue; - } else { - literal = match c { - '`' | '"' | '/' | '\'' => Some(c), - _ => None, - }; - } - - match c { - '(' | '[' | '{' => stack.push(c), - ')' | ']' | '}' => match (stack.pop(), c) { - (Some('('), ')') | (Some('['), ']') | (Some('{'), '}') => {} - (Some(left), _) => { - return Ok(ValidationResult::Invalid(Some(format!( - "Mismatched pairs: {:?} is not properly closed", - left - )))) - } - (None, _) => { - // While technically invalid when unpaired, it should be V8's task to output error instead. - // Thus marked as valid with no info. - return Ok(ValidationResult::Valid(None)); - } - }, - _ => {} - } - } - - if !stack.is_empty() || literal == Some('`') { - return Ok(ValidationResult::Incomplete); - } - - Ok(ValidationResult::Valid(None)) - } -} - -impl Highlighter for Helper { - fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { - hint.into() - } - - fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { - self.highlighter.highlight(line, pos) - } - - fn highlight_candidate<'c>( - &self, - candidate: &'c str, - _completion: rustyline::CompletionType, - ) -> Cow<'c, str> { - self.highlighter.highlight(candidate, 0) - } - - fn highlight_char(&self, line: &str, _: usize) -> bool { - !line.is_empty() - } -} - -struct LineHighlighter { - regex: Regex, -} - -impl LineHighlighter { - fn new() -> Self { - let regex = Regex::new( - r#"(?x) - (?P<comment>(?:/\*[\s\S]*?\*/|//[^\n]*)) | - (?P<string>(?:"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|`([^`\\]|\\.)*`)) | - (?P<regexp>/(?:(?:\\/|[^\n/]))*?/[gimsuy]*) | - (?P<number>\b\d+(?:\.\d+)?(?:e[+-]?\d+)*n?\b) | - (?P<infinity>\b(?:Infinity|NaN)\b) | - (?P<hexnumber>\b0x[a-fA-F0-9]+\b) | - (?P<octalnumber>\b0o[0-7]+\b) | - (?P<binarynumber>\b0b[01]+\b) | - (?P<boolean>\b(?:true|false)\b) | - (?P<null>\b(?:null)\b) | - (?P<undefined>\b(?:undefined)\b) | - (?P<keyword>\b(?:await|async|var|let|for|if|else|in|of|class|const|function|yield|return|with|case|break|switch|import|export|new|while|do|throw|catch|this)\b) | - "#, - ) - .unwrap(); - - Self { regex } - } -} - -impl Highlighter for LineHighlighter { - fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> { - self - .regex - .replace_all(&line.to_string(), |caps: &Captures<'_>| { - if let Some(cap) = caps.name("comment") { - colors::gray(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("string") { - colors::green(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("regexp") { - colors::red(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("number") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("boolean") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("null") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("undefined") { - colors::gray(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("keyword") { - colors::cyan(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("infinity") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("classes") { - colors::green_bold(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("hexnumber") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("octalnumber") { - colors::yellow(cap.as_str()).to_string() - } else if let Some(cap) = caps.name("binarynumber") { - colors::yellow(cap.as_str()).to_string() - } else { - caps[0].to_string() - } - }) - .to_string() - .into() - } -} - -async fn post_message_and_poll( - worker: &mut Worker, - session: &mut InspectorSession, - method: &str, - params: Option<Value>, -) -> Result<Value, AnyError> { - let response = session.post_message(method, params); - tokio::pin!(response); - - loop { - tokio::select! { - result = &mut response => { - return result - } - - _ = worker.run_event_loop() => { - // A zero delay is long enough to yield the thread in order to prevent the loop from - // running hot for messages that are taking longer to resolve like for example an - // evaluation of top level await. - tokio::time::delay_for(tokio::time::Duration::from_millis(0)).await; - } - } - } -} - -async fn read_line_and_poll( - worker: &mut Worker, - session: &mut InspectorSession, - 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("> ")); - - let mut poll_worker = true; - - loop { - for (method, params) in message_rx.try_iter() { - response_tx - .send(session.post_message(&method, params).await) - .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. - let mut timeout = - tokio::time::delay_for(tokio::time::Duration::from_millis(100)); - - tokio::select! { - result = &mut line => { - return result.unwrap(); - } - _ = worker.run_event_loop(), if poll_worker => { - poll_worker = false; - } - _ = &mut timeout => { - poll_worker = true - } - } - } -} - -static PRELUDE: &str = 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."); - }, -}); -"#; - -async fn inject_prelude( - worker: &mut MainWorker, - session: &mut InspectorSession, - context_id: u64, -) -> Result<(), AnyError> { - post_message_and_poll( - worker, - session, - "Runtime.evaluate", - Some(json!({ - "expression": PRELUDE, - "contextId": context_id, - })), - ) - .await?; - - Ok(()) -} - -pub async fn is_closing( - worker: &mut MainWorker, - session: &mut InspectorSession, - context_id: u64, -) -> Result<bool, AnyError> { - let closed = post_message_and_poll( - worker, - session, - "Runtime.evaluate", - Some(json!({ - "expression": "(globalThis.closed)", - "contextId": context_id, - })), - ) - .await? - .get("result") - .unwrap() - .get("value") - .unwrap() - .as_bool() - .unwrap(); - - Ok(closed) -} - -pub async fn run( - program_state: &ProgramState, - mut worker: MainWorker, -) -> Result<(), AnyError> { - let mut session = worker.create_inspector_session(); - - let history_file = program_state.dir.root.join("deno_history.txt"); - - post_message_and_poll(&mut *worker, &mut session, "Runtime.enable", None) - .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 (message_tx, message_rx) = sync_channel(1); - let (response_tx, response_rx) = channel(); - - let helper = Helper { - context_id, - message_tx, - response_rx, - highlighter: LineHighlighter::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()"); - - inject_prelude(&mut worker, &mut session, context_id).await?; - - while !is_closing(&mut worker, &mut session, context_id).await? { - let line = read_line_and_poll( - &mut *worker, - &mut session, - &message_rx, - &response_tx, - editor.clone(), - ) - .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 = post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.evaluate", - Some(json!({ - "expression": format!("'use strict'; void 0;\n{}", &wrapped_line), - "contextId": context_id, - "replMode": true, - })), - ) - .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 - { - post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.evaluate", - Some(json!({ - "expression": format!("'use strict'; void 0;\n{}", &line), - "contextId": context_id, - "replMode": true, - })), - ) - .await? - } else { - evaluate_response - }; - - let evaluate_result = evaluate_response.get("result").unwrap(); - let evaluate_exception_details = - evaluate_response.get("exceptionDetails"); - - if evaluate_exception_details.is_some() { - post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.callFunctionOn", - Some(json!({ - "executionContextId": context_id, - "functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }", - "arguments": [ - evaluate_result, - ], - })), - ).await?; - } else { - post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.callFunctionOn", - 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 = - post_message_and_poll( - &mut *worker, - &mut session, - "Runtime.callFunctionOn", - Some(json!({ - "executionContextId": 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(); - let output = match evaluate_exception_details { - Some(_) => format!("Uncaught {}", value), - None => value.to_string(), - }; - - println!("{}", output); - - editor.lock().unwrap().add_history_entry(line.as_str()); - } - Err(ReadlineError::Interrupted) => { - println!("exit using ctrl+d or close()"); - continue; - } - Err(ReadlineError::Eof) => { - break; - } - Err(err) => { - println!("Error: {:?}", err); - break; - } - } - } - - std::fs::create_dir_all(history_file.parent().unwrap())?; - editor - .lock() - .unwrap() - .save_history(history_file.to_str().unwrap())?; - - Ok(()) -} |