yerba 0.2.2-x86_64-linux-gnu → 0.4.0-x86_64-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +167 -12
  3. data/exe/x86_64-linux-gnu/yerba +0 -0
  4. data/ext/yerba/extconf.rb +39 -12
  5. data/ext/yerba/include/yerba.h +21 -10
  6. data/ext/yerba/yerba.c +91 -25
  7. data/lib/yerba/3.2/yerba.so +0 -0
  8. data/lib/yerba/3.3/yerba.so +0 -0
  9. data/lib/yerba/3.4/yerba.so +0 -0
  10. data/lib/yerba/4.0/yerba.so +0 -0
  11. data/lib/yerba/collection.rb +35 -0
  12. data/lib/yerba/document.rb +16 -0
  13. data/lib/yerba/sequence.rb +169 -1
  14. data/lib/yerba/version.rb +1 -1
  15. data/lib/yerba.rb +7 -2
  16. data/rust/Cargo.lock +1 -1
  17. data/rust/Cargo.toml +2 -2
  18. data/rust/cbindgen.toml +1 -0
  19. data/rust/rustfmt.toml +1 -1
  20. data/rust/src/commands/blank_lines.rs +1 -4
  21. data/rust/src/commands/delete.rs +9 -4
  22. data/rust/src/commands/directives.rs +61 -0
  23. data/rust/src/commands/get.rs +52 -26
  24. data/rust/src/commands/insert.rs +8 -4
  25. data/rust/src/commands/mod.rs +71 -9
  26. data/rust/src/commands/move_item.rs +2 -1
  27. data/rust/src/commands/move_key.rs +2 -1
  28. data/rust/src/commands/quote_style.rs +12 -6
  29. data/rust/src/commands/remove.rs +8 -4
  30. data/rust/src/commands/rename.rs +8 -4
  31. data/rust/src/commands/selectors.rs +158 -0
  32. data/rust/src/commands/set.rs +33 -16
  33. data/rust/src/commands/sort.rs +342 -10
  34. data/rust/src/didyoumean.rs +53 -0
  35. data/rust/src/document/condition.rs +139 -0
  36. data/rust/src/document/delete.rs +91 -0
  37. data/rust/src/document/get.rs +262 -0
  38. data/rust/src/document/insert.rs +314 -0
  39. data/rust/src/document/mod.rs +784 -0
  40. data/rust/src/document/set.rs +90 -0
  41. data/rust/src/document/sort.rs +607 -0
  42. data/rust/src/document/style.rs +473 -0
  43. data/rust/src/error.rs +35 -7
  44. data/rust/src/ffi.rs +213 -520
  45. data/rust/src/json.rs +1 -7
  46. data/rust/src/lib.rs +89 -2
  47. data/rust/src/main.rs +2 -0
  48. data/rust/src/quote_style.rs +83 -7
  49. data/rust/src/selector.rs +2 -7
  50. data/rust/src/syntax.rs +41 -21
  51. data/rust/src/yerbafile.rs +39 -18
  52. metadata +13 -3
  53. data/rust/src/document.rs +0 -2237
@@ -48,6 +48,22 @@ module Yerba
48
48
  end
49
49
  end
50
50
 
51
+ def find_by(...)
52
+ root.find_by(...)
53
+ end
54
+
55
+ def where(...)
56
+ root.where(...)
57
+ end
58
+
59
+ def pluck(...)
60
+ root.pluck(...)
61
+ end
62
+
63
+ def <<(item)
64
+ root << item
65
+ end
66
+
51
67
  def inspect
52
68
  if path
53
69
  "#<Yerba::Document path=#{path.inspect}>"
@@ -58,7 +58,100 @@ module Yerba
58
58
  def each
59
59
  return enum_for(:each) unless block_given?
60
60
 
61
- length.times { |i| yield self[i] }
61
+ length.times { |index| yield self[index] }
62
+ end
63
+
64
+ def find_by(selector = nil, value = nil, **criteria)
65
+ index = index_of(selector, value, **criteria)
66
+
67
+ self[index] if index
68
+ end
69
+
70
+ def where(selector = nil, value = nil, **criteria)
71
+ indices_of(selector, value, **criteria).map { |index| self[index] }
72
+ end
73
+
74
+ def pluck(*fields)
75
+ return [] unless @document
76
+
77
+ all_values = @document.get_value(@selector)
78
+ return [] unless all_values.is_a?(Array)
79
+
80
+ if fields.length == 1
81
+
82
+ all_values.map { |item| item.is_a?(Hash) ? item[fields.first.to_s] : item }
83
+ else
84
+
85
+ all_values.map { |item| fields.map(&:to_s).map { |field| item.is_a?(Hash) ? item[field] : nil } }
86
+ end
87
+ end
88
+
89
+ def index_of(selector = nil, value = nil, **criteria)
90
+ if selector && value.nil? && criteria.empty?
91
+ values = @document&.get("#{@selector}[]")
92
+
93
+ return values.index(selector) if values.is_a?(Array)
94
+
95
+ return nil
96
+ end
97
+
98
+ criteria[selector] = value if selector && value
99
+ pairs = expand_nested_criteria(criteria)
100
+
101
+ indices = nil
102
+
103
+ pairs.each do |field, expected|
104
+ field_string = field.to_s
105
+
106
+ if field_string.include?("[]")
107
+ matching = nested_indices_for(field_string, expected)
108
+ else
109
+ values = @document&.get("#{@selector}[].#{field_string}")
110
+
111
+ next unless values.is_a?(Array)
112
+
113
+ matching = values.each_with_index.filter_map { |actual, index| index if actual == expected }
114
+ end
115
+
116
+ indices = indices ? indices & matching : matching
117
+ end
118
+
119
+ indices&.first
120
+ end
121
+
122
+ def indices_of(selector = nil, value = nil, **criteria)
123
+ if selector && value.nil? && criteria.empty?
124
+ values = @document&.get("#{@selector}[]")
125
+
126
+ if values.is_a?(Array)
127
+ return values.each_with_index.filter_map { |actual, index| index if actual == selector }
128
+ end
129
+
130
+ return []
131
+ end
132
+
133
+ criteria[selector] = value if selector && value
134
+ pairs = expand_nested_criteria(criteria)
135
+
136
+ indices = nil
137
+
138
+ pairs.each do |field, expected|
139
+ field_string = field.to_s
140
+
141
+ if field_string.include?("[]")
142
+ matching = nested_indices_for(field_string, expected)
143
+ else
144
+ values = @document&.get("#{@selector}[].#{field_string}")
145
+
146
+ next unless values.is_a?(Array)
147
+
148
+ matching = values.each_with_index.filter_map { |actual, index| index if actual == expected }
149
+ end
150
+
151
+ indices = indices ? indices & matching : matching
152
+ end
153
+
154
+ indices || []
62
155
  end
63
156
 
64
157
  def length
@@ -69,6 +162,7 @@ module Yerba
69
162
  scalar_items.length
70
163
  else
71
164
  data = @document.get_value(@selector)
165
+
72
166
  data.is_a?(Array) ? data.length : 0
73
167
  end
74
168
  else
@@ -156,6 +250,79 @@ module Yerba
156
250
 
157
251
  private
158
252
 
253
+ def expand_nested_criteria(criteria)
254
+ expanded = []
255
+
256
+ criteria.each do |field, value|
257
+ if value.is_a?(Hash)
258
+ flatten_hash("#{field}[]", value).each do |path, leaf_value|
259
+ expanded << [path, leaf_value]
260
+ end
261
+ elsif value.is_a?(Array)
262
+ value.each do |item|
263
+ expanded << ["#{field}[]", item]
264
+ end
265
+ else
266
+ expanded << [field, value]
267
+ end
268
+ end
269
+
270
+ expanded
271
+ end
272
+
273
+ def flatten_hash(prefix, hash)
274
+ result = {}
275
+
276
+ hash.each do |key, value|
277
+ path = "#{prefix}.#{key}"
278
+
279
+ if value.is_a?(Hash)
280
+ flatten_hash("#{path}[]", value).each { |nested_path, leaf| result[nested_path] = leaf }
281
+ else
282
+ result[path] = value
283
+ end
284
+ end
285
+
286
+ result
287
+ end
288
+
289
+ def nested_indices_for(field, expected)
290
+ all_values = @document&.get_value(@selector)
291
+ return [] unless all_values.is_a?(Array)
292
+
293
+ all_values.each_with_index.filter_map do |item, index|
294
+ next unless item.is_a?(Hash)
295
+
296
+ nested_values = dig_values(item, field)
297
+ index if nested_values.include?(expected)
298
+ end
299
+ end
300
+
301
+ def dig_values(hash, path)
302
+ parts = path.split(".")
303
+ current = [hash]
304
+
305
+ parts.each do |part|
306
+ next_values = []
307
+
308
+ current.each do |value|
309
+ if part == "[]" && value.is_a?(Array)
310
+ next_values.concat(value)
311
+ elsif part.end_with?("[]")
312
+ key = part.chomp("[]")
313
+ child = value.is_a?(Hash) ? value[key] : nil
314
+ next_values.concat(child) if child.is_a?(Array)
315
+ elsif value.is_a?(Hash)
316
+ next_values << value[part] if value.key?(part)
317
+ end
318
+ end
319
+
320
+ current = next_values
321
+ end
322
+
323
+ current
324
+ end
325
+
159
326
  def format_for_insert(value)
160
327
  Formatting.quote(value, detect_quote_style)
161
328
  end
@@ -172,6 +339,7 @@ module Yerba
172
339
  result
173
340
  else
174
341
  data = @document.get_value(@selector)
342
+
175
343
  data.is_a?(Array) ? data : []
176
344
  end
177
345
  else
data/lib/yerba/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Yerba
4
- VERSION = "0.2.2"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/yerba.rb CHANGED
@@ -12,9 +12,14 @@ require_relative "yerba/document"
12
12
  require_relative "yerba/collection"
13
13
 
14
14
  begin
15
- require "yerba/yerba"
15
+ major, minor, = RUBY_VERSION.split(".")
16
+ require "yerba/#{major}.#{minor}/yerba"
16
17
  rescue LoadError
17
- # C extension not available, fall back to CLI mode
18
+ begin
19
+ require "yerba/yerba"
20
+ rescue LoadError
21
+ # C extension not available, fall back to CLI mode
22
+ end
18
23
  end
19
24
 
20
25
  module Yerba
data/rust/Cargo.lock CHANGED
@@ -784,7 +784,7 @@ dependencies = [
784
784
 
785
785
  [[package]]
786
786
  name = "yerba"
787
- version = "0.2.2"
787
+ version = "0.4.0"
788
788
  dependencies = [
789
789
  "cbindgen",
790
790
  "clap",
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "yerba"
3
- version = "0.2.2"
3
+ version = "0.4.0"
4
4
  edition = "2021"
5
5
  authors = ["Marco Roth <marco.roth@intergga.ch>"]
6
6
  description = "YAML Editing and Refactoring with Better Accuracy"
@@ -13,7 +13,7 @@ categories = ["parser-implementations", "development-tools"]
13
13
  [lib]
14
14
  name = "yerba"
15
15
  path = "src/lib.rs"
16
- crate-type = ["cdylib", "rlib"]
16
+ crate-type = ["cdylib", "staticlib", "rlib"]
17
17
 
18
18
  [[bin]]
19
19
  name = "yerba"
data/rust/cbindgen.toml CHANGED
@@ -9,6 +9,7 @@ no_includes = true
9
9
 
10
10
  [export]
11
11
  include = [
12
+ "NodeType",
12
13
  "YerbaValueType",
13
14
  "YerbaResult",
14
15
  "YerbaTypedValue",
data/rust/rustfmt.toml CHANGED
@@ -1,2 +1,2 @@
1
1
  tab_spaces = 2
2
- max_width = 120
2
+ max_width = 160
@@ -39,10 +39,7 @@ impl Args {
39
39
  } else {
40
40
  use super::color::*;
41
41
 
42
- eprintln!(
43
- "{RED}Error:{RESET} expected a number for blank line count, got '{}'",
44
- self.first
45
- );
42
+ eprintln!("{RED}Error:{RESET} expected a number for blank line count, got '{}'", self.first);
46
43
 
47
44
  std::process::exit(1);
48
45
  };
@@ -3,12 +3,13 @@ use std::sync::LazyLock;
3
3
  use indoc::indoc;
4
4
 
5
5
  use super::colorize_examples;
6
- use super::{output, parse_file, run_op};
6
+ use super::{output, parse_file, resolve_files, run_op};
7
7
 
8
8
  static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
9
9
  colorize_examples(indoc! {r#"
10
10
  yerba delete config.yml "database.pool"
11
11
  yerba delete videos.yml "[0].description"
12
+ yerba delete "data/**/event.yml" "date_precision"
12
13
  yerba delete config.yml "database.pool" --dry-run
13
14
  "#})
14
15
  });
@@ -28,8 +29,12 @@ pub struct Args {
28
29
 
29
30
  impl Args {
30
31
  pub fn run(self) {
31
- let mut document = parse_file(&self.file);
32
- run_op(|| document.delete(&self.selector));
33
- output(&self.file, &document, self.dry_run);
32
+ for resolved_file in resolve_files(&self.file) {
33
+ let mut document = parse_file(&resolved_file);
34
+ let result = document.delete(&self.selector);
35
+
36
+ run_op(&resolved_file, &document, result);
37
+ output(&resolved_file, &document, self.dry_run);
38
+ }
34
39
  }
35
40
  }
@@ -0,0 +1,61 @@
1
+ use std::sync::LazyLock;
2
+
3
+ use indoc::indoc;
4
+
5
+ use super::colorize_examples;
6
+ use super::{output, parse_file, resolve_files};
7
+
8
+ static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
9
+ colorize_examples(indoc! {r#"
10
+ yerba directives config.yml --ensure
11
+ yerba directives config.yml --remove
12
+ yerba directives "data/**/*.yml" --ensure
13
+ yerba directives config.yml --ensure --dry-run
14
+ "#})
15
+ });
16
+
17
+ #[derive(clap::Args)]
18
+ #[command(
19
+ about = "Add or remove the document start marker (---)",
20
+ arg_required_else_help = true,
21
+ after_help = EXAMPLES.as_str()
22
+ )]
23
+ pub struct Args {
24
+ file: String,
25
+ /// Add --- if missing
26
+ #[arg(long)]
27
+ ensure: bool,
28
+ /// Remove --- if present
29
+ #[arg(long)]
30
+ remove: bool,
31
+ #[arg(long)]
32
+ dry_run: bool,
33
+ }
34
+
35
+ impl Args {
36
+ pub fn run(self) {
37
+ if !self.ensure && !self.remove {
38
+ use super::color::*;
39
+ eprintln!("{RED}Error:{RESET} specify --ensure or --remove");
40
+ std::process::exit(1);
41
+ }
42
+
43
+ if self.ensure && self.remove {
44
+ use super::color::*;
45
+ eprintln!("{RED}Error:{RESET} --ensure and --remove are mutually exclusive");
46
+ std::process::exit(1);
47
+ }
48
+
49
+ for resolved_file in resolve_files(&self.file) {
50
+ let mut document = parse_file(&resolved_file);
51
+
52
+ if self.ensure {
53
+ let _ = document.ensure_directives();
54
+ } else if self.remove {
55
+ let _ = document.remove_directives();
56
+ }
57
+
58
+ output(&resolved_file, &document, self.dry_run);
59
+ }
60
+ }
61
+ }
@@ -3,7 +3,7 @@ use std::sync::LazyLock;
3
3
 
4
4
  use indoc::indoc;
5
5
 
6
- use super::{colorize_examples, parse_file, resolve_files};
6
+ use super::{colorize_examples, parse_file, resolve_files, show_similar_selectors};
7
7
 
8
8
  static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
9
9
  colorize_examples(indoc! {r#"
@@ -64,9 +64,42 @@ impl Args {
64
64
 
65
65
  let search_path_string = search_path.to_selector_string();
66
66
  let mut all_results: Vec<serde_json::Value> = Vec::new();
67
+ let files = resolve_files(&self.file);
68
+ let is_glob = files.len() > 1;
67
69
 
68
- for resolved_file in resolve_files(&self.file) {
69
- let document = parse_file(&resolved_file);
70
+ for resolved_file in &files {
71
+ let document = parse_file(resolved_file);
72
+
73
+ if normalized_condition.is_none() && !document.exists(&self.selector) {
74
+ if is_glob {
75
+ continue;
76
+ }
77
+
78
+ use super::color::*;
79
+
80
+ eprintln!("{RED}Error:{RESET} selector \"{}\" not found in {}", self.selector, resolved_file);
81
+
82
+ show_similar_selectors(resolved_file, &document, &self.selector);
83
+ process::exit(1);
84
+ }
85
+
86
+ if let Some(fields) = &select_fields {
87
+ for field in fields {
88
+ let field_trimmed = field.trim().trim_start_matches('.');
89
+ let full_selector = if search_path_string.is_empty() {
90
+ field_trimmed.to_string()
91
+ } else {
92
+ format!("{}.{}", search_path_string, field_trimmed)
93
+ };
94
+
95
+ if !document.exists(&full_selector) {
96
+ use super::color::*;
97
+ eprintln!("{RED}Error:{RESET} select field \"{}\" not found in {}", field.trim(), resolved_file);
98
+ show_similar_selectors(resolved_file, &document, &full_selector);
99
+ process::exit(1);
100
+ }
101
+ }
102
+ }
70
103
 
71
104
  let values: Vec<serde_yaml::Value> = if let Some(condition) = &normalized_condition {
72
105
  document.filter(&search_path_string, condition)
@@ -74,16 +107,6 @@ impl Args {
74
107
  document.get_values(&search_path_string)
75
108
  };
76
109
 
77
- if values.is_empty()
78
- && !selector.has_brackets()
79
- && normalized_condition.is_none()
80
- && !document.exists(&self.selector)
81
- {
82
- use super::color::*;
83
- eprintln!("{RED}Error:{RESET} path not found: {}", self.selector);
84
- process::exit(1);
85
- }
86
-
87
110
  for value in values {
88
111
  if let Some(field) = &extract_field {
89
112
  let field_string = field.to_selector_string();
@@ -121,6 +144,19 @@ impl Args {
121
144
  }
122
145
  }
123
146
 
147
+ if is_glob && all_results.is_empty() && normalized_condition.is_none() {
148
+ use super::color::*;
149
+
150
+ eprintln!(
151
+ "{RED}Error:{RESET} selector \"{}\" not found in any of the {} files matching \"{}\"",
152
+ self.selector,
153
+ files.len(),
154
+ self.file
155
+ );
156
+
157
+ process::exit(1);
158
+ }
159
+
124
160
  if self.raw {
125
161
  for value in &all_results {
126
162
  match value {
@@ -132,23 +168,13 @@ impl Args {
132
168
  }
133
169
  }
134
170
  } else if all_results.len() == 1 {
135
- println!(
136
- "{}",
137
- serde_json::to_string_pretty(&all_results[0]).unwrap_or_else(|_| "null".to_string())
138
- );
171
+ println!("{}", serde_json::to_string_pretty(&all_results[0]).unwrap_or_else(|_| "null".to_string()));
139
172
  } else {
140
- println!(
141
- "{}",
142
- serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string())
143
- );
173
+ println!("{}", serde_json::to_string_pretty(&all_results).unwrap_or_else(|_| "[]".to_string()));
144
174
  }
145
175
  }
146
176
 
147
- fn resolve_search_scope(
148
- &self,
149
- selector: &yerba::Selector,
150
- condition_path: Option<&yerba::Selector>,
151
- ) -> (yerba::Selector, Option<yerba::Selector>) {
177
+ fn resolve_search_scope(&self, selector: &yerba::Selector, condition_path: Option<&yerba::Selector>) -> (yerba::Selector, Option<yerba::Selector>) {
152
178
  if let Some(condition) = condition_path {
153
179
  if condition.is_relative() {
154
180
  let (container, field) = selector.split_at_last_bracket();
@@ -3,7 +3,7 @@ use std::sync::LazyLock;
3
3
  use indoc::indoc;
4
4
 
5
5
  use super::colorize_examples;
6
- use super::{output, parse_file, run_op};
6
+ use super::{output, parse_file, resolve_files, run_op};
7
7
 
8
8
  static EXAMPLES: LazyLock<String> = LazyLock::new(|| {
9
9
  colorize_examples(indoc! {r#"
@@ -99,8 +99,12 @@ impl Args {
99
99
  .unwrap_or(yerba::InsertPosition::Last)
100
100
  };
101
101
 
102
- let mut document = parse_file(&self.file);
103
- run_op(|| document.insert_into(&self.selector, &resolved_value, position));
104
- output(&self.file, &document, self.dry_run);
102
+ for resolved_file in resolve_files(&self.file) {
103
+ let mut document = parse_file(&resolved_file);
104
+ let result = document.insert_into(&self.selector, &resolved_value, position.clone());
105
+
106
+ run_op(&resolved_file, &document, result);
107
+ output(&resolved_file, &document, self.dry_run);
108
+ }
105
109
  }
106
110
  }
@@ -2,6 +2,7 @@ pub mod apply;
2
2
  pub mod blank_lines;
3
3
  pub mod check;
4
4
  pub mod delete;
5
+ pub mod directives;
5
6
  pub mod get;
6
7
  pub mod init;
7
8
  pub mod insert;
@@ -11,6 +12,7 @@ pub mod move_key;
11
12
  pub mod quote_style;
12
13
  pub mod remove;
13
14
  pub mod rename;
15
+ pub mod selectors;
14
16
  pub mod set;
15
17
  pub mod sort;
16
18
  pub mod sort_keys;
@@ -125,10 +127,7 @@ pub(crate) fn colorize_help(input: &str) -> String {
125
127
  }
126
128
 
127
129
  if columns.len() == 3 {
128
- output.push_str(&format!(
129
- " \x1b[36m{:<20}{RESET} {:<21} {DIM}{}{RESET}\n",
130
- columns[0], columns[1], columns[2]
131
- ));
130
+ output.push_str(&format!(" \x1b[36m{:<20}{RESET} {:<21} {DIM}{}{RESET}\n", columns[0], columns[1], columns[2]));
132
131
  } else if columns.len() == 2 {
133
132
  output.push_str(&format!(" \x1b[36m{:<20}{RESET} {}\n", columns[0], columns[1]));
134
133
  } else {
@@ -153,6 +152,8 @@ pub enum Command {
153
152
  Sort(sort::Args),
154
153
  QuoteStyle(quote_style::Args),
155
154
  BlankLines(blank_lines::Args),
155
+ Directives(directives::Args),
156
+ Selectors(selectors::Args),
156
157
  #[command(about = "Create a new Yerbafile in the current directory")]
157
158
  Init,
158
159
  #[command(about = "Apply all rules from the Yerbafile and write changes")]
@@ -180,6 +181,8 @@ impl Command {
180
181
  Command::Sort(args) => args.run(),
181
182
  Command::QuoteStyle(args) => args.run(),
182
183
  Command::BlankLines(args) => args.run(),
184
+ Command::Directives(args) => args.run(),
185
+ Command::Selectors(args) => args.run(),
183
186
  Command::Init => init::run(),
184
187
  Command::Apply => apply::run(),
185
188
  Command::Check => check::run(),
@@ -224,7 +227,7 @@ pub(crate) fn run_yerbafile(write: bool) {
224
227
  }
225
228
 
226
229
  if !has_changes && !has_errors {
227
- eprintln!(" {GREEN}All files match the rules.{RESET}");
230
+ eprintln!("\n{BOLD}{GREEN}All files match the rules.{RESET}");
228
231
  }
229
232
 
230
233
  if !write && has_changes {
@@ -327,13 +330,72 @@ pub(crate) fn parse_file(file: &str) -> yerba::Document {
327
330
  })
328
331
  }
329
332
 
330
- pub(crate) fn run_op(operation: impl FnOnce() -> Result<(), yerba::YerbaError>) {
333
+ pub(crate) fn run_op(file: &str, document: &yerba::Document, result: Result<(), yerba::YerbaError>) {
334
+ run_op_with_hint(file, document, result, None);
335
+ }
336
+
337
+ pub(crate) fn run_op_with_hint(file: &str, document: &yerba::Document, result: Result<(), yerba::YerbaError>, hint: Option<&str>) {
331
338
  use color::*;
332
339
 
333
- operation().unwrap_or_else(|error| {
334
- eprintln!("{RED}Error:{RESET} {}", error);
340
+ if let Err(error) = result {
341
+ if let yerba::YerbaError::SelectorNotFound(selector) = &error {
342
+ eprintln!("{RED}Error:{RESET} selector \"{selector}\" not found in {file}");
343
+
344
+ show_similar_selectors(file, document, selector);
345
+ } else {
346
+ eprintln!("{RED}Error:{RESET} {}", error);
347
+ }
348
+
349
+ if let Some(hint) = hint {
350
+ if matches!(error, yerba::YerbaError::SelectorNotFound(_)) {
351
+ eprintln!();
352
+ eprintln!(" {DIM}Hint: {hint}{RESET}");
353
+ }
354
+ }
355
+
335
356
  process::exit(1);
336
- });
357
+ }
358
+ }
359
+
360
+ pub(crate) fn show_similar_selectors(file: &str, document: &yerba::Document, invalid_path: &str) {
361
+ use color::*;
362
+ use yerba::didyoumean::didyoumean_ranked;
363
+
364
+ let selectors = document.selectors();
365
+
366
+ if selectors.is_empty() {
367
+ return;
368
+ }
369
+
370
+ let query = invalid_path.split_whitespace().next().unwrap_or(invalid_path);
371
+ let threshold = query.len() / 2 + 3;
372
+ let close = didyoumean_ranked(query, &selectors, threshold);
373
+
374
+ eprintln!();
375
+
376
+ if close.is_empty() {
377
+ eprintln!(" {BOLD}Available selectors in {file}:{RESET}");
378
+
379
+ for selector in selectors.iter().take(10) {
380
+ eprintln!(" {DIM}{selector}{RESET}");
381
+ }
382
+
383
+ if selectors.len() > 10 {
384
+ eprintln!(" {DIM}... and {} more{RESET}", selectors.len() - 10);
385
+ }
386
+ } else if close.len() == 1 {
387
+ eprintln!(" {BOLD}Did you mean this selector?{RESET} {}", close[0]);
388
+ } else {
389
+ eprintln!(" {BOLD}Did you mean one of these selectors?{RESET}");
390
+
391
+ for selector in close.iter().take(5) {
392
+ eprintln!(" {selector}");
393
+ }
394
+ }
395
+
396
+ eprintln!();
397
+ eprintln!(" {BOLD}To see all valid selectors, run:{RESET}");
398
+ eprintln!(" yerba selectors \"{file}\"{RESET}");
337
399
  }
338
400
 
339
401
  pub(crate) fn output(file: &str, document: &yerba::Document, dry_run: bool) {
@@ -48,7 +48,8 @@ impl Args {
48
48
  |document, path, reference| document.resolve_sequence_index(path, reference),
49
49
  );
50
50
 
51
- run_op(|| document.move_item(&self.selector, from_index, to_index));
51
+ let result = document.move_item(&self.selector, from_index, to_index);
52
+ run_op(&self.file, &document, result);
52
53
  output(&self.file, &document, self.dry_run);
53
54
  }
54
55
  }
@@ -81,7 +81,8 @@ impl Args {
81
81
  |document, parent_path, reference| document.resolve_key_index(parent_path, reference),
82
82
  );
83
83
 
84
- run_op(|| document.move_key(parent_path, from_index, to_index));
84
+ let result = document.move_key(parent_path, from_index, to_index);
85
+ run_op(&self.file, &document, result);
85
86
  output(&self.file, &document, self.dry_run);
86
87
  }
87
88
  }