Skip to content

Commit

Permalink
Use single quotes around completions with space/quote/doublequotes
Browse files Browse the repository at this point in the history
  • Loading branch information
domsleee committed Sep 5, 2024
1 parent 3fbdca2 commit 290e137
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 40 deletions.
54 changes: 54 additions & 0 deletions src/completer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::nu_util;
use reedline::{Completer, Suggestion};
use std::path::Path;

pub fn get_suggestions(nu_file_data: &[u8], pwd: &Path, arg_str: &str) -> Vec<String> {
let mut completer = nu_util::extern_completer(pwd, nu_file_data);
let suggestions = completer.complete(arg_str, arg_str.len());
let suggestion_strings: Vec<String> = suggestions.iter().map(get_suggestion_string).collect();
suggestion_strings
}

fn get_suggestion_string(suggestion: &Suggestion) -> String {
let arg = suggestion.value.clone().to_string();
if arg.len() > 1 && arg.starts_with('`') && arg.ends_with('`') {
// replace the start and end backticks with single quotes
// also replace all single quotes with double single quote
// e.g. completion of `git add ` with a file with a space in it
let replaced_single_quotes = &arg[1..arg.len() - 1].replace('\'', "''");
return format!("'{replaced_single_quotes}'",);
}

// for example, `git ch` should be `checkout`, not `git checkout`
if suggestion.span.start == 0 {
return arg.split(' ').last().unwrap().to_string();
}

// if the arg contains a space, double quote, or single quote, wrap it in single quotes
if !arg.is_empty()
&& !arg.starts_with('\'')
&& (arg.contains(' ') || arg.contains('\"') || arg.contains('\''))
{
let replaced_arg = arg.replace('\'', "''");
return format!("'{replaced_arg}'");
}

arg.to_string()
}

#[cfg(test)]
mod tests {
use crate::DEFAULT_CONFIG_DATA;

use super::*;

#[test]
fn test_get_suggestions() {
let nu_file_data: &[u8] = DEFAULT_CONFIG_DATA;
let pwd = std::env::current_dir().unwrap();
let arg_str = "git checkou";
let expected = vec!["checkout".to_string()];
let suggestions = get_suggestions(nu_file_data, &pwd, arg_str);
assert_eq!(suggestions, expected);
}
}
24 changes: 4 additions & 20 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
extern crate log;
use args::RootArgs;
use clap::Parser;
use completer::get_suggestions;
use itertools::Itertools;
use posh_tabcomplete::TABCOMPLETE_FILE;
use reedline::Completer;
use regex::Regex;
use std::{
collections::HashSet,
Expand All @@ -15,6 +15,7 @@ use std::{
use crate::args::{CompleteArgs, TabCompleteSubCommand};

mod args;
mod completer;
mod nu_util;
use std::str;

Expand All @@ -32,7 +33,7 @@ pub fn run_with_args(root_args: &RootArgs) -> Result<(), std::io::Error> {
Ok(())
}

static DEFAULT_CONFIG_DATA: &[u8] = include_bytes!("../resource/completions.nu");
pub static DEFAULT_CONFIG_DATA: &[u8] = include_bytes!("../resource/completions.nu");
fn complete(root_args: &RootArgs, complete_args: &CompleteArgs) -> Result<(), std::io::Error> {
let arg_str = &complete_args.args_str;
let pwd = env::current_dir()?;
Expand All @@ -43,28 +44,11 @@ fn complete(root_args: &RootArgs, complete_args: &CompleteArgs) -> Result<(), st
} else {
string_from_files.as_bytes()
};
let mut completer = nu_util::extern_completer(&pwd, nu_file_data);
let suggestions = completer.complete(arg_str, arg_str.len());
let suggestion_strings: Vec<String> = suggestions
.iter()
.map(|x| maybe_process_path(&x.value.clone().to_string()))
.collect();

let suggestion_strings: Vec<String> = get_suggestions(nu_file_data, &pwd, arg_str);
println!("{}", suggestion_strings.join("\n"));
Ok(())
}

fn maybe_process_path(arg: &str) -> String {
if arg.len() > 1 && arg.starts_with('`') && arg.ends_with('`') {
// replace the start and end backticks with single quotes
// also replace all single quotes with double single quote
let replaced_single_quotes = &arg[1..arg.len() - 1].replace('\'', "''");
return format!("'{replaced_single_quotes}'",);
}

arg.split(' ').last().unwrap().to_string()
}

fn get_string_from_files(root_args: &RootArgs) -> String {
match &root_args.file_override {
Some(file_override) => fs::read_to_string(file_override)
Expand Down
54 changes: 52 additions & 2 deletions tests/test_complete_npm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,56 @@ pub use testenv::*;
#[apply(shell_to_use)]
fn test_npm_run(shell: &str) {
let lines = util::run_command(shell, "Invoke-TabComplete 'npm run '");
assert_that!(lines).contains("script1".to_string());
assert_that!(lines).contains("script2".to_string());
assert_eq!(lines, vec!["script1", "script2"]);
}

#[apply(shell_to_use)]
fn test_npm_run_space(shell: &str) -> Result<(), std::io::Error> {
let test_env = TestEnv::new(shell).create_package_json(
r#"
{
"scripts": {
"with space": "echo hello world",
}
}
"#,
)?;

let lines = util::run_with_test_env(&test_env, "Invoke-TabComplete 'npm run with'");
assert_eq!(&lines, &["'with space'".to_string()]);
Ok(())
}

#[apply(shell_to_use)]
fn test_npm_run_single_quotes(shell: &str) -> Result<(), std::io::Error> {
let test_env = TestEnv::new(shell).create_package_json(
r#"
{
"scripts": {
"withsingle'quote": "echo hello world",
}
}
"#,
)?;

let lines = util::run_with_test_env(&test_env, "Invoke-TabComplete 'npm run with'");
assert_that!(&lines).equals_iterator(&["'withsingle''quote'".to_string()].iter());
Ok(())
}

#[apply(shell_to_use)]
fn test_npm_run_double_quotes(shell: &str) -> Result<(), std::io::Error> {
let test_env = TestEnv::new(shell).create_package_json(
r#"
{
"scripts": {
"withdouble\"quote": "echo hello world",
}
}
"#,
)?;

let lines = util::run_with_test_env(&test_env, "Invoke-TabComplete 'npm run with'");
assert_that!(&lines).equals_iterator(&[r#"'withdouble"quote'"#.to_string()].iter());
Ok(())
}
46 changes: 28 additions & 18 deletions tests/testenv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ impl TestEnv {

let (temp_dir, profile_path) =
create_working_dir(profile_prefix_data).expect("create successful");

let package_json_path = temp_dir.path().join("package.json");
let package_json_str = r#"
{
"name": "my-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"script1": "node index.js",
"script2": "echo \"Error: no test specified\" && exit 1"
}
}
"#;
create_package_json(&package_json_path, package_json_str)
.expect("Able to create package json");

TestEnv {
shell: shell.to_string(),
temp_dir,
Expand Down Expand Up @@ -74,7 +90,7 @@ impl TestEnv {
}

pub fn run_with_profile(&self, command: &str) -> Result<Output, io::Error> {
let root = self.temp_dir.path();
let root: &Path = self.temp_dir.path();
let file_contents = format!(". {}\n{command}", self.profile_path.to_str().unwrap());
let file_path = root.join("file.ps1");
fs::File::create(&file_path)?.write_all(file_contents.as_bytes())?;
Expand All @@ -97,6 +113,12 @@ impl TestEnv {

Ok(output)
}

pub fn create_package_json(self, package_json_contents: &str) -> Result<TestEnv, io::Error> {
let package_json_path = self.temp_dir.path().join("package.json");
create_package_json(&package_json_path, package_json_contents)?;
Ok(self)
}
}

fn prepend_to_path_var(path: &Path) -> OsString {
Expand Down Expand Up @@ -145,30 +167,18 @@ fn create_working_dir(profile_prefix_data: Vec<&str>) -> Result<(TempDir, PathBu
run_git(&["checkout", "-b", "testbranch23"]);
run_git(&["remote", "add", "origin", "[email protected]"]);

let package_json_path = root.join("package.json");
create_package_json(&package_json_path)?;

profile_path
};

Ok((temp_dir, profile_path))
}

fn create_package_json(package_json_path: &Path) -> Result<(), io::Error> {
let package_json_str = r#"
{
"name": "my-app",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"script1": "node index.js",
"script2": "echo \"Error: no test specified\" && exit 1"
}
}
"#;

fn create_package_json(
package_json_path: &Path,
package_json_contents: &str,
) -> Result<(), io::Error> {
let mut file = fs::File::create(package_json_path)?;
file.write_all(package_json_str.as_bytes())?;
file.write_all(package_json_contents.as_bytes())?;

Ok(())
}
Expand Down
4 changes: 4 additions & 0 deletions tests/testenv/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ use crate::TestEnv;

pub fn run_command(shell: &str, command: &str) -> Vec<String> {
let testenv = TestEnv::new(shell);
run_with_test_env(&testenv, command)
}

pub fn run_with_test_env(testenv: &TestEnv, command: &str) -> Vec<String> {
let res = testenv.run_with_profile(command).unwrap();
res.stdout.lines().map(|x| x.unwrap()).collect()
}

0 comments on commit 290e137

Please sign in to comment.