yerba 0.7.2 → 0.7.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8f244827c16c50ecb2f86a8ba94220e92dd49967527330a5a9d0544517b88d7
4
- data.tar.gz: 1be11c5a28e591c0511851144508d85e0df4a0b423087affc3f62f4166491713
3
+ metadata.gz: bfa98c0309844cc745efbdea0a559825a797ca9eba6eaf2d63e39b2e5098333c
4
+ data.tar.gz: 270f11efd66edf472dba1a5a524db66bcdc24f1eba081c4e5b267853f275111b
5
5
  SHA512:
6
- metadata.gz: e34070fd0393841cebee7d98a86f6fd7e1608585acb0047a5456f0411813aecdc3ccda1798a83302bcf7859c4efc116fd72d3deeadfeb9ca3fbeb5e5e1ee4d33
7
- data.tar.gz: 1378d5c099549a33d415d2dc754fba875449c1a440bb71487e697f0e2591635d998db4a7373f4633523d1e1a3c84b8bcf3738f0290c3cc9e5a3c6c918c007a09
6
+ metadata.gz: 2bf52ab64c03b3eaeafaefe547b4cbe3cf67c21c19025aa21da00afe4e36edbc2d8bf3ae33ce06d744fa8616da37aed189e7e91afab3cc904fec63151aeef667
7
+ data.tar.gz: edc83ce4ee21753401e258deb33e54c7a206c13ab8b909a9d54595295e396caf0a3eaa58a33746205d5a955d8b62d04da0e9473423adcf118bc1a1cd6a955d67
data/ext/yerba/yerba.c CHANGED
@@ -986,10 +986,12 @@ static VALUE collection_s_get(VALUE self, VALUE pattern, VALUE path) {
986
986
 
987
987
  VALUE line = rb_hash_aref(item, rb_str_new_cstr("line"));
988
988
  VALUE kwargs = rb_hash_new();
989
+ VALUE location = rb_hash_aref(item, rb_str_new_cstr("location"));
989
990
 
990
991
  rb_hash_aset(kwargs, ID2SYM(rb_intern("selector")), selector);
991
992
  if (!NIL_P(file_path)) rb_hash_aset(kwargs, ID2SYM(rb_intern("file_path")), file_path);
992
993
  if (!NIL_P(line)) rb_hash_aset(kwargs, ID2SYM(rb_intern("line")), line);
994
+ if (!NIL_P(location)) rb_hash_aset(kwargs, ID2SYM(rb_intern("location")), location);
993
995
 
994
996
  if (strcmp(type_str, "scalar") == 0) {
995
997
  VALUE text = rb_hash_aref(item, rb_str_new_cstr("text"));
data/lib/yerba/map.rb CHANGED
@@ -123,6 +123,28 @@ module Yerba
123
123
  end
124
124
  end
125
125
 
126
+ # Deletes a key from the map, or deletes the entire map node from the document.
127
+ #
128
+ # When +key+ is given and the map is connected to a document, deletes that key
129
+ # from the map in the YAML document. When no +key+ is given and the map is
130
+ # connected, removes the entire map node at its selector path. When not connected
131
+ # to a document, delegates to the underlying Hash.
132
+ #
133
+ # Returns +self+.
134
+ #
135
+ # # Delete a specific key from a connected map:
136
+ # document = Yerba::Document.parse(<<~YAML)
137
+ # database:
138
+ # host: localhost
139
+ # pool: 10
140
+ # YAML
141
+ #
142
+ # document["database"].delete("pool")
143
+ # document.to_s # => "database:\n host: localhost\n"
144
+ #
145
+ # # Delete the entire map node:
146
+ # document["database"].delete
147
+ # document.to_s # => "{}\n"
126
148
  def delete(key = nil)
127
149
  if key && connected?
128
150
  new_path = @selector.empty? ? key.to_s : "#{@selector}.#{key}"
data/lib/yerba/node.rb CHANGED
@@ -36,14 +36,20 @@ module Yerba
36
36
  instance
37
37
  end
38
38
 
39
- def from(file_path:, selector:, line: nil, **attributes)
39
+ def from(file_path:, selector:, line: nil, location: nil, **attributes)
40
40
  instance = allocate
41
41
 
42
- instance.send(:init_node, nil, selector, nil, nil, file_path, line)
42
+ instance.send(:init_node, nil, selector, coerce_location(location), nil, file_path, line)
43
43
  instance.send(:init_from, **attributes) if instance.respond_to?(:init_from, true)
44
44
 
45
45
  instance
46
46
  end
47
+
48
+ def coerce_location(location)
49
+ return location unless location.is_a?(Hash)
50
+
51
+ Location.new(**location.transform_keys(&:to_sym))
52
+ end
47
53
  end
48
54
 
49
55
  def self.included(base)
@@ -258,6 +258,21 @@ module Yerba
258
258
  self
259
259
  end
260
260
 
261
+ # Deletes the entire sequence node from the YAML document.
262
+ #
263
+ # Removes the sequence at its selector path from the parent document.
264
+ # Has no effect if the sequence is not connected to a document.
265
+ #
266
+ # Returns the document, or +nil+ if the sequence is not connected to a document.
267
+ #
268
+ # document = Yerba::Document.parse(<<~YAML)
269
+ # items:
270
+ # - name: Ruby
271
+ # - name: Rust
272
+ # YAML
273
+ #
274
+ # document["items"].delete
275
+ # document.to_s # => "{}\n"
261
276
  def delete
262
277
  document&.delete(@selector)
263
278
  end
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.7.2"
4
+ VERSION = "0.7.4"
5
5
  end
data/rust/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "yerba"
3
- version = "0.7.2"
3
+ version = "0.7.4"
4
4
  edition = "2021"
5
5
  authors = ["Marco Roth <marco.roth@intergga.ch>"]
6
6
  description = "YAML Editing and Refactoring with Better Accuracy"
@@ -11,7 +11,7 @@ impl Document {
11
11
  }
12
12
 
13
13
  pub fn filter_with_selectors(&self, dot_path: &str, condition: &str) -> Vec<(yaml_serde::Value, String, usize)> {
14
- let source = self.root.text().to_string();
14
+ let source = self.source_text();
15
15
 
16
16
  self
17
17
  .navigate_all_compact(dot_path)
@@ -19,7 +19,7 @@ impl Document {
19
19
  .filter(|node| self.evaluate_condition_on_node(node, condition))
20
20
  .map(|node| {
21
21
  let offset: usize = node.text_range().start().into();
22
- let line = source[..offset].matches('\n').count() + 1;
22
+ let line = line_at(&source, offset);
23
23
 
24
24
  (node_to_yaml_value(node), super::get::node_selector(node), line)
25
25
  })
@@ -52,7 +52,7 @@ impl Document {
52
52
  }
53
53
 
54
54
  for node in &target_nodes {
55
- if let Some(sequence) = node.descendants().find_map(BlockSeq::cast) {
55
+ if let Some(sequence) = find_block_sequence(node) {
56
56
  for entry in sequence.entries() {
57
57
  if let Some(text) = entry.flow().and_then(|flow| extract_scalar_text(flow.syntax())) {
58
58
  if text == right {
@@ -67,7 +67,7 @@ impl Document {
67
67
  }
68
68
  "not_contains" => {
69
69
  for node in &target_nodes {
70
- if let Some(sequence) = node.descendants().find_map(BlockSeq::cast) {
70
+ if let Some(sequence) = find_block_sequence(node) {
71
71
  for entry in sequence.entries() {
72
72
  if let Some(text) = entry.flow().and_then(|flow| extract_scalar_text(flow.syntax())) {
73
73
  if text == right {
@@ -11,12 +11,9 @@ impl Document {
11
11
 
12
12
  if source_parent == destination_parent {
13
13
  let (parent_path, source_key) = source_path.rsplit_once('.').unwrap_or(("", source_path));
14
- let parent_node = self.navigate(parent_path)?;
15
14
 
16
- let map = parent_node
17
- .descendants()
18
- .find_map(BlockMap::cast)
19
- .ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
15
+ let parent_node = self.navigate(parent_path)?;
16
+ let map = find_block_map(&parent_node).ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
20
17
 
21
18
  let entry = find_entry_by_key(&map, source_key).ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
22
19
  let key_node = entry.key().ok_or_else(|| YerbaError::SelectorNotFound(source_path.to_string()))?;
@@ -42,14 +39,12 @@ impl Document {
42
39
 
43
40
  match last_segment {
44
41
  crate::selector::SelectorSegment::Key(last_key) => {
45
- let has_wildcard = selector.has_wildcard() || selector.has_brackets();
42
+ let has_wildcard = selector.has_wildcard();
43
+ let has_brackets = selector.has_brackets();
46
44
 
47
- if parent_path.is_empty() && !has_wildcard {
45
+ if parent_path.is_empty() && (!has_wildcard || !has_brackets) {
48
46
  let parent_node = self.navigate(&parent_path)?;
49
- let map = parent_node
50
- .descendants()
51
- .find_map(BlockMap::cast)
52
- .ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
47
+ let map = find_block_map(&parent_node).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
53
48
 
54
49
  let entry = find_entry_by_key(&map, last_key).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
55
50
 
@@ -66,38 +61,30 @@ impl Document {
66
61
  return Err(YerbaError::SelectorNotFound(dot_path.to_string()));
67
62
  }
68
63
 
69
- if parent_nodes.len() == 1 && !has_wildcard {
70
- let map = parent_nodes[0]
71
- .descendants()
72
- .find_map(BlockMap::cast)
73
- .ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
74
-
64
+ if parent_nodes.len() == 1 && (!has_wildcard || !has_brackets) {
65
+ let map = find_block_map(&parent_nodes[0]).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
75
66
  let entry = find_entry_by_key(&map, last_key).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
76
67
 
68
+ if map.entries().count() == 1 {
69
+ return self.collapse_to_empty_flow_map(&parent_nodes[0]);
70
+ }
71
+
77
72
  return self.remove_map_entry(&entry);
78
73
  }
79
74
 
80
75
  let mut ranges: Vec<TextRange> = Vec::new();
81
76
 
82
77
  for parent_node in &parent_nodes {
83
- if let Some(map) = parent_node.descendants().find_map(BlockMap::cast) {
78
+ if let Some(map) = find_block_map(parent_node) {
84
79
  if let Some(entry) = find_entry_by_key(&map, last_key) {
85
80
  ranges.push(removal_range(entry.syntax()));
86
81
  }
87
82
  }
88
83
  }
89
84
 
90
- if ranges.is_empty() {
91
- return Ok(());
92
- }
85
+ let edits = ranges.into_iter().map(|range| (range, String::new())).collect();
93
86
 
94
- ranges.reverse();
95
-
96
- for range in ranges {
97
- self.apply_edit(range, "")?;
98
- }
99
-
100
- Ok(())
87
+ self.apply_edits(edits)
101
88
  }
102
89
 
103
90
  crate::selector::SelectorSegment::Index(index) => self.remove_at(&parent_path, *index),
@@ -108,10 +95,7 @@ impl Document {
108
95
  pub fn remove(&mut self, dot_path: &str, value: &str) -> Result<(), YerbaError> {
109
96
  let current_node = self.navigate(dot_path)?;
110
97
 
111
- let sequence = current_node
112
- .descendants()
113
- .find_map(BlockSeq::cast)
114
- .ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;
98
+ let sequence = find_block_sequence(&current_node).ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;
115
99
 
116
100
  let target_entry = sequence
117
101
  .entries()
@@ -124,6 +108,10 @@ impl Document {
124
108
  })
125
109
  .ok_or_else(|| YerbaError::SelectorNotFound(format!("{} item '{}'", dot_path, value)))?;
126
110
 
111
+ if sequence.entries().count() == 1 {
112
+ return self.collapse_to_empty_flow_sequence(&current_node);
113
+ }
114
+
127
115
  self.remove_node(target_entry.syntax())
128
116
  }
129
117
 
@@ -132,10 +120,7 @@ impl Document {
132
120
 
133
121
  let current_node = self.navigate(dot_path)?;
134
122
 
135
- let sequence = current_node
136
- .descendants()
137
- .find_map(BlockSeq::cast)
138
- .ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;
123
+ let sequence = find_block_sequence(&current_node).ok_or_else(|| YerbaError::NotASequence(dot_path.to_string()))?;
139
124
 
140
125
  let entries: Vec<_> = sequence.entries().collect();
141
126
 
@@ -143,6 +128,67 @@ impl Document {
143
128
  return Err(YerbaError::IndexOutOfBounds(index, entries.len()));
144
129
  }
145
130
 
131
+ if entries.len() == 1 {
132
+ return self.collapse_to_empty_flow_sequence(&current_node);
133
+ }
134
+
146
135
  self.remove_node(entries[index].syntax())
147
136
  }
137
+
138
+ fn collapse_to_empty_flow_map(&mut self, current_node: &SyntaxNode) -> Result<(), YerbaError> {
139
+ let map_value = current_node
140
+ .descendants()
141
+ .find(|descendant| descendant.kind() == SyntaxKind::BLOCK_MAP_VALUE)
142
+ .ok_or_else(|| YerbaError::SelectorNotFound("Expected a map value".to_string()))?;
143
+
144
+ let mut range = map_value.text_range();
145
+
146
+ if let Some(previous) = map_value.prev_sibling_or_token().and_then(|element| element.into_token()) {
147
+ if previous.kind() == SyntaxKind::WHITESPACE && previous.text().contains('\n') {
148
+ range = TextRange::new(previous.text_range().start(), range.end());
149
+ }
150
+ }
151
+
152
+ self.apply_edit(range, " {}")
153
+ }
154
+
155
+ fn collapse_to_empty_flow_sequence(&mut self, current_node: &SyntaxNode) -> Result<(), YerbaError> {
156
+ if current_node.kind() == SyntaxKind::BLOCK_MAP_VALUE {
157
+ let mut range = current_node.text_range();
158
+ let mut comment_text: Option<String> = None;
159
+ let mut sibling = current_node.prev_sibling_or_token();
160
+
161
+ while let Some(ref element) = sibling {
162
+ match element {
163
+ rowan::NodeOrToken::Token(token) => {
164
+ if token.kind() == SyntaxKind::COMMENT {
165
+ comment_text = Some(token.text().to_string());
166
+ range = TextRange::new(token.text_range().start(), range.end());
167
+ sibling = token.prev_sibling_or_token();
168
+ } else if token.kind() == SyntaxKind::WHITESPACE {
169
+ range = TextRange::new(token.text_range().start(), range.end());
170
+ sibling = token.prev_sibling_or_token();
171
+ } else {
172
+ break;
173
+ }
174
+ }
175
+ _ => break,
176
+ }
177
+ }
178
+
179
+ let replacement = match comment_text {
180
+ Some(comment) => format!(" [] {}", comment),
181
+ None => " []".to_string(),
182
+ };
183
+
184
+ return self.apply_edit(range, &replacement);
185
+ }
186
+
187
+ let sequence = current_node
188
+ .descendants()
189
+ .find_map(BlockSeq::cast)
190
+ .ok_or_else(|| YerbaError::NotASequence("Expected a sequence".to_string()))?;
191
+
192
+ self.apply_edit(removal_range(sequence.syntax()), "[]")
193
+ }
148
194
  }
@@ -83,15 +83,17 @@ impl Document {
83
83
  }
84
84
 
85
85
  pub fn get_all_located(&self, dot_path: &str) -> Vec<LocatedNode> {
86
- let source = self.root.text().to_string();
86
+ let source = self.source_text();
87
87
  let file_path = self.path.as_ref().map(|p| p.to_string_lossy().to_string());
88
88
 
89
89
  self
90
90
  .navigate_all_compact(dot_path)
91
91
  .iter()
92
92
  .map(|node| {
93
- let offset: usize = node.text_range().start().into();
94
- let line = source[..offset].matches('\n').count() + 1;
93
+ let range = node.text_range();
94
+ let offset: usize = range.start().into();
95
+ let line = line_at(&source, offset);
96
+ let location = compute_location(&source, offset, range.end().into());
95
97
  let selector = node_selector(node);
96
98
  let is_scalar = !node
97
99
  .descendants()
@@ -108,13 +110,8 @@ impl Document {
108
110
  let node_type = if is_scalar {
109
111
  "scalar"
110
112
  } else {
111
- let map_pos = node.descendants().find_map(BlockMap::cast).map(|m| m.syntax().text_range().start());
112
- let seq_pos = node.descendants().find_map(BlockSeq::cast).map(|s| s.syntax().text_range().start());
113
-
114
- match (map_pos, seq_pos) {
115
- (Some(m), Some(s)) if s < m => "sequence",
116
- (Some(_), _) => "map",
117
- (None, Some(_)) => "sequence",
113
+ match first_collection(node) {
114
+ Some(FirstCollection::Sequence(_)) => "sequence",
118
115
  _ => "map",
119
116
  }
120
117
  };
@@ -126,6 +123,7 @@ impl Document {
126
123
  file_path: file_path.clone(),
127
124
  selector,
128
125
  line,
126
+ location,
129
127
  }
130
128
  })
131
129
  .collect()
@@ -142,7 +140,7 @@ impl Document {
142
140
 
143
141
  pub fn get_node_info(&self, dot_path: &str) -> NodeInfo {
144
142
  let selector = crate::selector::Selector::parse(dot_path);
145
- let source = self.root.text().to_string();
143
+ let source = self.source_text();
146
144
 
147
145
  let (location, key_name, key_location) = self.resolve_location(dot_path, &source);
148
146
 
@@ -364,7 +362,7 @@ impl Document {
364
362
  self.navigate(parent_path).ok()?
365
363
  };
366
364
 
367
- let map = parent_node.descendants().find_map(BlockMap::cast)?;
365
+ let map = find_block_map(&parent_node)?;
368
366
 
369
367
  find_entry_by_key(&map, last_key).map(|entry| entry.syntax().clone())
370
368
  }
@@ -375,7 +373,7 @@ impl Document {
375
373
  Err(_) => return Vec::new(),
376
374
  };
377
375
 
378
- let sequence = match current_node.descendants().find_map(BlockSeq::cast) {
376
+ let sequence = match find_block_sequence(&current_node) {
379
377
  Some(sequence) => sequence,
380
378
  None => return Vec::new(),
381
379
  };
@@ -403,17 +401,16 @@ impl Document {
403
401
  pub fn get_sequence_indent(&self, dot_path: &str) -> Option<&'static str> {
404
402
  let current_node = self.navigate(dot_path).ok()?;
405
403
 
406
- let sequence = current_node.descendants().find_map(BlockSeq::cast)?;
404
+ let sequence = find_block_sequence(&current_node)?;
407
405
  let first_entry = sequence.entries().next()?;
408
406
 
409
407
  let entry_indent = preceding_whitespace_indent(first_entry.syntax());
410
408
 
411
409
  let entry_node = current_node.ancestors().find(|ancestor| ancestor.kind() == SyntaxKind::BLOCK_MAP_ENTRY)?;
412
410
 
413
- let source = self.root.text().to_string();
411
+ let source = self.source_text();
414
412
  let entry_start: usize = entry_node.text_range().start().into();
415
- let line_start = source[..entry_start].rfind('\n').map(|position| position + 1).unwrap_or(0);
416
- let key_indent = &source[line_start..entry_start];
413
+ let key_indent = &source[line_start_at(&source, entry_start)..entry_start];
417
414
 
418
415
  if entry_indent.len() > key_indent.len() {
419
416
  Some("indented")