From d0b4ad2046d57dd9874d11d5a3c832ca8a4ee964 Mon Sep 17 00:00:00 2001 From: Ade Attwood Date: Sat, 11 May 2024 10:39:22 +0100 Subject: [PATCH 1/8] test: add regex feature tests --- tests/features.rs | 2 +- tests/features/regex.feature | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/features/regex.feature diff --git a/tests/features.rs b/tests/features.rs index 4b8c7bf..fb4ad95 100644 --- a/tests/features.rs +++ b/tests/features.rs @@ -24,5 +24,5 @@ fn assert_output(world: &mut TestWorld, expected: String) { } fn main() { - futures::executor::block_on(TestWorld::run("tests/features/basic.feature")); + futures::executor::block_on(TestWorld::run("tests/features")); } diff --git a/tests/features/regex.feature b/tests/features/regex.feature new file mode 100644 index 0000000..cc77786 --- /dev/null +++ b/tests/features/regex.feature @@ -0,0 +1,19 @@ +Feature: Regex search and replace + + Scenario: You can search and replace with with a regular expression + Given Search is '(\w+)' + And Replace is 'new' + And Input is 'This is a' + Then Output is 'new new new' + + Scenario: You can use a '$' to replace a match group + Given Search is 'function (\w+)\(\)' + And Replace is 'fun $1()' + And Input is 'function foo()' + Then Output is 'fun foo()' + + Scenario: You can will need to wrap the match group when the match is against another word + Given Search is 'Hello (\w+)' + And Replace is 'Hello ${1}s' + And Input is 'Hello world' + Then Output is 'Hello worlds' From 08d96f8ea428c9b91c5d0a26752aa71e17b94b23 Mon Sep 17 00:00:00 2001 From: Ade Attwood Date: Sat, 11 May 2024 15:15:51 +0100 Subject: [PATCH 2/8] feat: add case preserving search and replace --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + src/lib.rs | 22 +++++++++++++++++++--- tests/features/case-perserving.feature | 7 +++++++ tests/features/regex.feature | 2 +- 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 tests/features/case-perserving.feature diff --git a/Cargo.lock b/Cargo.lock index a7d341b..1f1de34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,16 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "cruet" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6132609543972496bc97b1e01f1ce6586768870aeb4cabeb3385f4e05b5caead" +dependencies = [ + "once_cell", + "regex", +] + [[package]] name = "cucumber" version = "0.20.2" @@ -272,6 +282,7 @@ name = "dev_case" version = "0.1.0" dependencies = [ "clap", + "cruet", "cucumber", "futures", "regex", diff --git a/Cargo.toml b/Cargo.toml index 6dcf58d..2b53e87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] clap = { version = "4.5.4", features = ["derive"] } +cruet = "0.14.0" regex = "1.10.4" [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 3da331f..ee75c56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,23 @@ use regex::Regex; +fn concert_replacement(original: &str, replacement: &str) -> String { + if cruet::is_camel_case(original) { + cruet::to_camel_case(replacement) + } else if cruet::is_kebab_case(original) { + cruet::to_kebab_case(replacement) + } else if cruet::is_pascal_case(original) { + cruet::to_pascal_case(replacement) + } else if cruet::is_snake_case(original) { + cruet::to_snake_case(replacement) + } else { + replacement.to_string() + } +} + pub fn replace(search: &String, replace: String, input: String) -> String { let mut index = 0; let mut output = input; - let search_pattern = Regex::new(search).unwrap(); + let search_pattern = Regex::new(&format!("(?i){search}")).unwrap(); while let Some(search_match) = search_pattern.find_at(&output, index) { let start = search_match.start(); @@ -19,8 +33,10 @@ pub fn replace(search: &String, replace: String, input: String) -> String { } }; - index = start + replacement.len(); - output.replace_range(start..end, &replacement); + let converted_replacement = concert_replacement(&output[start..end], &replacement); + output.replace_range(start..end, &converted_replacement); + + index = start + converted_replacement.len(); } output diff --git a/tests/features/case-perserving.feature b/tests/features/case-perserving.feature new file mode 100644 index 0000000..00a3655 --- /dev/null +++ b/tests/features/case-perserving.feature @@ -0,0 +1,7 @@ +Feature: Case preserving search and replace + + Scenario: You can search and replace with with a regular expression + Given Search is 'productid' + And Replace is 'catalogId' + And Input is 'function GetProductId(productId)' + Then Output is 'function GetCatalogId(catalogId)' diff --git a/tests/features/regex.feature b/tests/features/regex.feature index cc77786..0e07ec5 100644 --- a/tests/features/regex.feature +++ b/tests/features/regex.feature @@ -3,7 +3,7 @@ Feature: Regex search and replace Scenario: You can search and replace with with a regular expression Given Search is '(\w+)' And Replace is 'new' - And Input is 'This is a' + And Input is 'this is a' Then Output is 'new new new' Scenario: You can use a '$' to replace a match group From bb03e0976444c1d53f7b115507c9860f5e91ec89 Mon Sep 17 00:00:00 2001 From: Ade Attwood Date: Sat, 11 May 2024 15:20:43 +0100 Subject: [PATCH 3/8] feat: add case preserving search and replace --- Cargo.lock | 11 +++++++++++ Cargo.toml | 1 + src/lib.rs | 22 +++++++++++++++++++--- tests/features/case-perserving.feature | 7 +++++++ tests/features/regex.feature | 2 +- 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 tests/features/case-perserving.feature diff --git a/Cargo.lock b/Cargo.lock index a7d341b..1f1de34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,16 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "cruet" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6132609543972496bc97b1e01f1ce6586768870aeb4cabeb3385f4e05b5caead" +dependencies = [ + "once_cell", + "regex", +] + [[package]] name = "cucumber" version = "0.20.2" @@ -272,6 +282,7 @@ name = "dev_case" version = "0.1.0" dependencies = [ "clap", + "cruet", "cucumber", "futures", "regex", diff --git a/Cargo.toml b/Cargo.toml index 6dcf58d..2b53e87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] clap = { version = "4.5.4", features = ["derive"] } +cruet = "0.14.0" regex = "1.10.4" [dev-dependencies] diff --git a/src/lib.rs b/src/lib.rs index 3da331f..ee75c56 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,23 @@ use regex::Regex; +fn concert_replacement(original: &str, replacement: &str) -> String { + if cruet::is_camel_case(original) { + cruet::to_camel_case(replacement) + } else if cruet::is_kebab_case(original) { + cruet::to_kebab_case(replacement) + } else if cruet::is_pascal_case(original) { + cruet::to_pascal_case(replacement) + } else if cruet::is_snake_case(original) { + cruet::to_snake_case(replacement) + } else { + replacement.to_string() + } +} + pub fn replace(search: &String, replace: String, input: String) -> String { let mut index = 0; let mut output = input; - let search_pattern = Regex::new(search).unwrap(); + let search_pattern = Regex::new(&format!("(?i){search}")).unwrap(); while let Some(search_match) = search_pattern.find_at(&output, index) { let start = search_match.start(); @@ -19,8 +33,10 @@ pub fn replace(search: &String, replace: String, input: String) -> String { } }; - index = start + replacement.len(); - output.replace_range(start..end, &replacement); + let converted_replacement = concert_replacement(&output[start..end], &replacement); + output.replace_range(start..end, &converted_replacement); + + index = start + converted_replacement.len(); } output diff --git a/tests/features/case-perserving.feature b/tests/features/case-perserving.feature new file mode 100644 index 0000000..00a3655 --- /dev/null +++ b/tests/features/case-perserving.feature @@ -0,0 +1,7 @@ +Feature: Case preserving search and replace + + Scenario: You can search and replace with with a regular expression + Given Search is 'productid' + And Replace is 'catalogId' + And Input is 'function GetProductId(productId)' + Then Output is 'function GetCatalogId(catalogId)' diff --git a/tests/features/regex.feature b/tests/features/regex.feature index cc77786..0e07ec5 100644 --- a/tests/features/regex.feature +++ b/tests/features/regex.feature @@ -3,7 +3,7 @@ Feature: Regex search and replace Scenario: You can search and replace with with a regular expression Given Search is '(\w+)' And Replace is 'new' - And Input is 'This is a' + And Input is 'this is a' Then Output is 'new new new' Scenario: You can use a '$' to replace a match group From 568e61810601768d4eee4f00b71262d43b5804c2 Mon Sep 17 00:00:00 2001 From: Ade Attwood Date: Sat, 11 May 2024 21:35:56 +0100 Subject: [PATCH 4/8] feat: add input argument to read input from text or stdin You can now provide input as the content you want to search and replace in. If you don't provide input, the program will read from stdin. --- src/main.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1eaafda..65df5bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,17 +11,26 @@ struct Args { /// The replacement pattern. #[arg(short, long)] replace: String, + + /// The input content to search and replace. If not provided, input will be read from stdin. + #[arg(short, long)] + input: Option, } fn main() { let args = Args::parse(); - let mut input = String::new(); - match std::io::stdin().read_to_string(&mut input) { - Ok(_) => (), - Err(err) => { - eprintln!("Error reading from stdin: {}", err); - return; + let input = match args.input { + Some(input) => input, + None => { + let mut input = String::new(); + match std::io::stdin().read_to_string(&mut input) { + Ok(_) => input, + Err(err) => { + eprintln!("Error reading from stdin: {}", err); + return; + } + } } }; From dc5fbc402625e36e6e0df618b4b0600341927c75 Mon Sep 17 00:00:00 2001 From: Ade Attwood Date: Sat, 11 May 2024 21:41:22 +0100 Subject: [PATCH 5/8] fix: handle invalid regex patterns by returning the input string Right now we don't want to be panicking if the user provides an invalid regex. We also don't really want to be throwing or returning an error, this will mess with any live preview that is going on in external tools. We should return the input and let any preview display the text. This will happen if the user is doing some preview as you type kind of thing. --- src/lib.rs | 5 ++++- tests/features/regex.feature | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index ee75c56..e8d0a94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,10 @@ fn concert_replacement(original: &str, replacement: &str) -> String { pub fn replace(search: &String, replace: String, input: String) -> String { let mut index = 0; let mut output = input; - let search_pattern = Regex::new(&format!("(?i){search}")).unwrap(); + let search_pattern = match Regex::new(&format!("(?i){search}")) { + Ok(pattern) => pattern, + Err(_) => return output + }; while let Some(search_match) = search_pattern.find_at(&output, index) { let start = search_match.start(); diff --git a/tests/features/regex.feature b/tests/features/regex.feature index 0e07ec5..3e3ad5c 100644 --- a/tests/features/regex.feature +++ b/tests/features/regex.feature @@ -17,3 +17,9 @@ Feature: Regex search and replace And Replace is 'Hello ${1}s' And Input is 'Hello world' Then Output is 'Hello worlds' + + Scenario: You can search with an invalid regular expression + Given Search is '(\w+' + And Replace is 'new' + And Input is 'this is a' + Then Output is 'this is a' From 9fc7a737ebc1612e5ee571a0ae0e93dcaeef0e32 Mon Sep 17 00:00:00 2001 From: Ade Attwood Date: Sat, 11 May 2024 21:44:16 +0100 Subject: [PATCH 6/8] fix: handle invalid regex patterns by returning the input string Right now we don't want to be panicking if the user provides an invalid regex. We also don't really want to be throwing or returning an error, this will mess with any live preview that is going on in external tools. We should return the input and let any preview display the text. This will happen if the user is doing some preview as you type kind of thing. --- src/lib.rs | 5 ++++- tests/features/regex.feature | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index ee75c56..e8d0a94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,10 @@ fn concert_replacement(original: &str, replacement: &str) -> String { pub fn replace(search: &String, replace: String, input: String) -> String { let mut index = 0; let mut output = input; - let search_pattern = Regex::new(&format!("(?i){search}")).unwrap(); + let search_pattern = match Regex::new(&format!("(?i){search}")) { + Ok(pattern) => pattern, + Err(_) => return output + }; while let Some(search_match) = search_pattern.find_at(&output, index) { let start = search_match.start(); diff --git a/tests/features/regex.feature b/tests/features/regex.feature index 0e07ec5..3e3ad5c 100644 --- a/tests/features/regex.feature +++ b/tests/features/regex.feature @@ -17,3 +17,9 @@ Feature: Regex search and replace And Replace is 'Hello ${1}s' And Input is 'Hello world' Then Output is 'Hello worlds' + + Scenario: You can search with an invalid regular expression + Given Search is '(\w+' + And Replace is 'new' + And Input is 'this is a' + Then Output is 'this is a' From 832428dbf128094460938374ca6d4ee60cb95d30 Mon Sep 17 00:00:00 2001 From: Ade Attwood Date: Sat, 11 May 2024 21:44:16 +0100 Subject: [PATCH 7/8] feat: add input argument to read input from text or stdin You can now provide input as the content you want to search and replace in. If you don't provide input, the program will read from stdin. --- src/main.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1eaafda..65df5bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,17 +11,26 @@ struct Args { /// The replacement pattern. #[arg(short, long)] replace: String, + + /// The input content to search and replace. If not provided, input will be read from stdin. + #[arg(short, long)] + input: Option, } fn main() { let args = Args::parse(); - let mut input = String::new(); - match std::io::stdin().read_to_string(&mut input) { - Ok(_) => (), - Err(err) => { - eprintln!("Error reading from stdin: {}", err); - return; + let input = match args.input { + Some(input) => input, + None => { + let mut input = String::new(); + match std::io::stdin().read_to_string(&mut input) { + Ok(_) => input, + Err(err) => { + eprintln!("Error reading from stdin: {}", err); + return; + } + } } }; From 817c994da8d8006a8cee35d1144f4c5f71a06a8a Mon Sep 17 00:00:00 2001 From: Ade Attwood Date: Sat, 11 May 2024 21:45:21 +0100 Subject: [PATCH 8/8] fix: handle invalid regex patterns by returning the input string Right now we don't want to be panicking if the user provides an invalid regex. We also don't really want to be throwing or returning an error, this will mess with any live preview that is going on in external tools. We should return the input and let any preview display the text. This will happen if the user is doing some preview as you type kind of thing. --- src/lib.rs | 5 ++++- tests/features/regex.feature | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index ee75c56..5ff0528 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,10 @@ fn concert_replacement(original: &str, replacement: &str) -> String { pub fn replace(search: &String, replace: String, input: String) -> String { let mut index = 0; let mut output = input; - let search_pattern = Regex::new(&format!("(?i){search}")).unwrap(); + let search_pattern = match Regex::new(&format!("(?i){search}")) { + Ok(pattern) => pattern, + Err(_) => return output, + }; while let Some(search_match) = search_pattern.find_at(&output, index) { let start = search_match.start(); diff --git a/tests/features/regex.feature b/tests/features/regex.feature index 0e07ec5..3e3ad5c 100644 --- a/tests/features/regex.feature +++ b/tests/features/regex.feature @@ -17,3 +17,9 @@ Feature: Regex search and replace And Replace is 'Hello ${1}s' And Input is 'Hello world' Then Output is 'Hello worlds' + + Scenario: You can search with an invalid regular expression + Given Search is '(\w+' + And Replace is 'new' + And Input is 'this is a' + Then Output is 'this is a'