Skip to content

Commit

Permalink
support auto-completion for console cmd (#3521)
Browse files Browse the repository at this point in the history
* support auto-completion for console cmd

* Get command names from Clap API
  • Loading branch information
pause125 authored Jul 10, 2022
1 parent 94c2ddb commit f1e4fb2
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 21 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion commons/scmd/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ edition = "2021"
anyhow = "1.0.41"
thiserror = "1.0"
serde = { version = "1.0.130", features = ["derive"] }
rustyline = "9.0.0"
rustyline = "9.1.2"
rustyline-derive = "0.6.0"
clap = { version = "3", features = ["derive"] }
serde_json = { version="1.0", features = ["arbitrary_precision"]}
rust-flatten-json = "0.2.0"
Expand Down
175 changes: 175 additions & 0 deletions commons/scmd/src/console.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright (c) The Starcoin Core Contributors
// SPDX-License-Identifier: Apache-2.0

use std::borrow::Cow::{self, Borrowed, Owned};

use once_cell::sync::Lazy;
use rustyline::completion::{extract_word, Completer, FilenameCompleter, Pair};
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::hint::Hinter;
use rustyline::validate::{self, MatchingBracketValidator, Validator};
use rustyline::Context;
use rustyline_derive::Helper;
use std::collections::HashSet;

use rustyline::{
config::CompletionType, error::ReadlineError, ColorMode, Config as ConsoleConfig, EditMode,
OutputStreamType,
};

pub static G_DEFAULT_CONSOLE_CONFIG: Lazy<ConsoleConfig> = Lazy::new(|| {
ConsoleConfig::builder()
.max_history_size(1000)
.history_ignore_space(true)
.history_ignore_dups(true)
.completion_type(CompletionType::List)
.auto_add_history(false)
.edit_mode(EditMode::Emacs)
.color_mode(ColorMode::Enabled)
.output_stream(OutputStreamType::Stdout)
.build()
});

const DEFAULT_BREAK_CHARS: [u8; 3] = [b' ', b'\t', b'\n'];

#[derive(Hash, Debug, PartialEq, Eq)]
pub(crate) struct CommandName {
cmd: String,
pre_cmd: String,
}

impl CommandName {
pub(crate) fn new(cmd: String, pre_cmd: String) -> Self {
Self { cmd, pre_cmd }
}
}
struct CommandCompleter {
cmds: HashSet<CommandName>,
}

impl CommandCompleter {
pub fn find_matches(&self, line: &str, pos: usize) -> rustyline::Result<(usize, Vec<Pair>)> {
let (start, word) = extract_word(line, pos, None, &DEFAULT_BREAK_CHARS);
let pre_cmd = line[..start].trim();

let matches = self
.cmds
.iter()
.filter_map(|hint| {
if hint.cmd.starts_with(word) && pre_cmd == hint.pre_cmd {
let mut replacement = hint.cmd.clone();
replacement += " ";
Some(Pair {
display: hint.cmd.to_string(),
replacement: replacement.to_string(),
})
} else {
None
}
})
.collect();
Ok((start, matches))
}
}

impl Completer for CommandCompleter {
type Candidate = Pair;

fn complete(
&self,
line: &str,
pos: usize,
_ctx: &Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
self.find_matches(line, pos)
}
}
impl Hinter for CommandCompleter {
type Hint = String;

fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<Self::Hint> {
None
}
}

#[derive(Helper)]
pub(crate) struct RLHelper {
file_completer: FilenameCompleter,
cmd_completer: CommandCompleter,
highlighter: MatchingBracketHighlighter,
validator: MatchingBracketValidator,
}

impl Completer for RLHelper {
type Candidate = Pair;

fn complete(
&self,
line: &str,
pos: usize,
ctx: &Context<'_>,
) -> Result<(usize, Vec<Pair>), ReadlineError> {
match self.cmd_completer.complete(line, pos, ctx) {
Ok((start, matches)) => {
if matches.is_empty() {
self.file_completer.complete(line, pos, ctx)
} else {
Ok((start, matches))
}
}
Err(e) => Err(e),
}
}
}

impl Hinter for RLHelper {
type Hint = String;

fn hint(&self, _line: &str, _pos: usize, _ctx: &Context<'_>) -> Option<String> {
None
}
}

impl Highlighter for RLHelper {
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
_default: bool,
) -> Cow<'b, str> {
Borrowed(prompt)
}

fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Owned("\x1b[1m".to_owned() + hint + "\x1b[m")
}

fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
self.highlighter.highlight(line, pos)
}

fn highlight_char(&self, line: &str, pos: usize) -> bool {
self.highlighter.highlight_char(line, pos)
}
}

impl Validator for RLHelper {
fn validate(
&self,
ctx: &mut validate::ValidationContext,
) -> rustyline::Result<validate::ValidationResult> {
self.validator.validate(ctx)
}

fn validate_while_typing(&self) -> bool {
self.validator.validate_while_typing()
}
}

pub(crate) fn init_helper(cmds: HashSet<CommandName>) -> RLHelper {
RLHelper {
file_completer: FilenameCompleter::new(),
cmd_completer: CommandCompleter { cmds },
highlighter: MatchingBracketHighlighter::new(),
validator: MatchingBracketValidator::new(),
}
}
61 changes: 41 additions & 20 deletions commons/scmd/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,26 @@
// Copyright (c) The Starcoin Core Contributors
// SPDX-License-Identifier: Apache-2.0

pub use crate::console::G_DEFAULT_CONSOLE_CONFIG;
use crate::console::{init_helper, CommandName, RLHelper};
use crate::error::CmdError;
use crate::{
print_action_result, CommandAction, CommandExec, CustomCommand, HistoryOp, OutputFormat,
};
use anyhow::Result;
use clap::{Arg, Command};
use clap::{ErrorKind, Parser};
use once_cell::sync::Lazy;
use rustyline::{error::ReadlineError, Config as ConsoleConfig, Editor};
use serde_json::Value;
use std::collections::HashMap;
use std::collections::HashSet;
use std::ffi::OsString;
use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;

pub use rustyline::{
config::CompletionType, error::ReadlineError, ColorMode, Config as ConsoleConfig, EditMode,
Editor,
};

pub static G_DEFAULT_CONSOLE_CONFIG: Lazy<ConsoleConfig> = Lazy::new(|| {
ConsoleConfig::builder()
.max_history_size(1000)
.history_ignore_space(true)
.history_ignore_dups(true)
.completion_type(CompletionType::List)
.auto_add_history(false)
.edit_mode(EditMode::Emacs)
.color_mode(ColorMode::Enabled)
.build()
});

static G_OUTPUT_FORMAT_ARG: &str = "output-format";

pub struct CmdContext<State, GlobalOpt>
Expand Down Expand Up @@ -306,7 +292,18 @@ where
let global_opt = Arc::new(global_opt);
let state = Arc::new(state);
let (config, history_file) = init_action(&app, global_opt.clone(), state.clone());
let mut rl = Editor::<()>::with_config(config);
let mut rl = Editor::<RLHelper>::with_config(config);
let cmd_sets = Self::get_command_names_recursively(&app, "".to_string(), 3)
.iter()
.map(|(a, b)| {
CommandName::new(
a.to_string(),
b.replace(&app_name[..], "").trim().to_string(),
)
})
.collect();

rl.set_helper(Some(init_helper(cmd_sets)));
if let Some(history_file) = history_file.as_ref() {
if !history_file.exists() {
if let Err(e) = File::create(history_file.as_path()) {
Expand Down Expand Up @@ -464,7 +461,7 @@ where
global_opt: Arc<GlobalOpt>,
state: Arc<State>,
quit_action: Box<dyn FnOnce(Command, GlobalOpt, State)>,
mut rl: Editor<()>,
mut rl: Editor<RLHelper>,
history_file: Option<PathBuf>,
) {
let global_opt = Arc::try_unwrap(global_opt)
Expand All @@ -480,4 +477,28 @@ where
}
quit_action(app, global_opt, state);
}

fn get_command_names_recursively(
app: &Command,
prepositive: String,
max_depth: u32,
) -> HashSet<(String, String)> {
if max_depth == 0 {
return HashSet::<(String, String)>::new();
}
let name = app.get_name();
let mut pre = prepositive;
if !pre.is_empty() {
pre.push(' ');
}
pre.push_str(name);

let mut set = HashSet::new();
for sub_app in app.get_subcommands() {
set.insert((sub_app.get_name().to_owned(), pre.clone()));
let sub_set = Self::get_command_names_recursively(sub_app, pre.clone(), max_depth - 1);
set.extend(sub_set);
}
set
}
}
1 change: 1 addition & 0 deletions commons/scmd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

mod action;
mod command;
mod console;
mod context;
pub mod error;
mod result;
Expand Down

0 comments on commit f1e4fb2

Please sign in to comment.