Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli/repl) add tab completion #7827

Merged
merged 36 commits into from
Oct 19, 2020
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f316f1a
feat(cli/repl) add tab completion
caspervonb Oct 5, 2020
01be79b
Lint
caspervonb Oct 5, 2020
ff255f5
Use lexical global names when line len is 0
caspervonb Oct 5, 2020
e8a9cde
Lint
caspervonb Oct 5, 2020
304ee82
Filter object expression candidates based on rhs expression
caspervonb Oct 5, 2020
6202fa4
Add bracket completion
caspervonb Oct 5, 2020
bc9ea87
Merge branch 'master' into feat-cli-repl-tab-completion
caspervonb Oct 7, 2020
6c4679a
Format
caspervonb Oct 7, 2020
b3668a7
End [ completion with ]
caspervonb Oct 7, 2020
48b562d
Lint
caspervonb Oct 7, 2020
0402adb
Merge branch 'master' into feat-cli-repl-tab-completion
caspervonb Oct 7, 2020
309adac
Borrow method
caspervonb Oct 7, 2020
af61617
Remove word completion
caspervonb Oct 7, 2020
6f950c1
Add multi-line and default globalThis completion
caspervonb Oct 11, 2020
3e16ffa
Merge branch 'master' into feat-cli-repl-tab-completion
caspervonb Oct 11, 2020
84dfd63
Format
caspervonb Oct 11, 2020
5338f2e
Format whitespace
caspervonb Oct 11, 2020
1856d1d
Lint
caspervonb Oct 11, 2020
15a9cb7
Lint
caspervonb Oct 11, 2020
1c1ba40
Fix end offset
caspervonb Oct 12, 2020
bc665e5
Add parens and braces as delimiters
caspervonb Oct 12, 2020
233c9d6
Discard delimiters
caspervonb Oct 12, 2020
a7567b9
Merge branch 'master' into feat-cli-repl-tab-completion
caspervonb Oct 12, 2020
208dc44
Fix context_id size
caspervonb Oct 12, 2020
617e64f
Comment
caspervonb Oct 12, 2020
5e2776c
Ensure there can be no overflow
caspervonb Oct 12, 2020
c73ab97
Off by one
caspervonb Oct 12, 2020
47267d9
Return early on error
caspervonb Oct 12, 2020
4f9ac27
Better bracket matching
caspervonb Oct 12, 2020
0b51c13
Tweak matches
caspervonb Oct 12, 2020
b2815c1
Replace grave
caspervonb Oct 12, 2020
32cc01a
Back to basics
caspervonb Oct 14, 2020
5861627
Merge branch 'master' into feat-cli-repl-tab-completion
caspervonb Oct 17, 2020
552bc7f
Add basic word boundaries
caspervonb Oct 17, 2020
b9b3a03
Format
caspervonb Oct 17, 2020
aa03faf
Merge branch 'master' into feat-cli-repl-tab-completion
bartlomieju Oct 19, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 152 additions & 28 deletions cli/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,124 @@ 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::{Completer, Helper, Hinter};
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 syntax specific helpers to the editor like validation for multi-line edits.
#[derive(Completer, Helper, Hinter)]
// 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,
Expand Down Expand Up @@ -174,17 +275,27 @@ async fn post_message_and_poll(

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() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this a stream and select on it below.

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(1000));
tokio::time::delay_for(tokio::time::Duration::from_millis(100));

tokio::select! {
result = &mut line => {
Expand All @@ -194,7 +305,7 @@ async fn read_line_and_poll(
poll_worker = false;
}
_ = &mut timeout => {
poll_worker = true
poll_worker = true
}
}
}
Expand Down Expand Up @@ -305,7 +416,13 @@ pub async fn run(
}
}

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(),
};

Expand All @@ -325,7 +442,14 @@ pub async fn run(
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, editor.clone()).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
Expand Down Expand Up @@ -378,30 +502,30 @@ pub async fn run(

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?;
&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?;
&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
Expand Down