yerba 0.3.0 → 0.4.1
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 +4 -4
- data/README.md +93 -8
- data/ext/yerba/extconf.rb +22 -3
- data/ext/yerba/include/yerba.h +32 -9
- data/ext/yerba/yerba.c +133 -25
- data/lib/yerba/collection.rb +35 -0
- data/lib/yerba/document.rb +45 -3
- data/lib/yerba/query_result.rb +50 -0
- data/lib/yerba/sequence.rb +187 -1
- data/lib/yerba/version.rb +1 -1
- data/lib/yerba/yerbafile.rb +45 -0
- data/lib/yerba.rb +2 -0
- data/rust/Cargo.lock +1 -0
- data/rust/Cargo.toml +3 -2
- data/rust/cbindgen.toml +1 -0
- data/rust/rustfmt.toml +1 -1
- data/rust/src/commands/apply.rs +11 -2
- data/rust/src/commands/blank_lines.rs +1 -4
- data/rust/src/commands/check.rs +11 -2
- data/rust/src/commands/directives.rs +61 -0
- data/rust/src/commands/get.rs +5 -22
- data/rust/src/commands/mod.rs +16 -18
- data/rust/src/commands/quote_style.rs +12 -6
- data/rust/src/commands/selectors.rs +3 -19
- data/rust/src/commands/sort.rs +22 -157
- data/rust/src/didyoumean.rs +2 -4
- data/rust/src/document/condition.rs +139 -0
- data/rust/src/document/delete.rs +91 -0
- data/rust/src/document/get.rs +262 -0
- data/rust/src/document/insert.rs +384 -0
- data/rust/src/document/mod.rs +784 -0
- data/rust/src/document/set.rs +100 -0
- data/rust/src/document/sort.rs +639 -0
- data/rust/src/document/style.rs +473 -0
- data/rust/src/error.rs +24 -6
- data/rust/src/ffi.rs +272 -518
- data/rust/src/json.rs +1 -7
- data/rust/src/lib.rs +88 -2
- data/rust/src/main.rs +2 -0
- data/rust/src/quote_style.rs +83 -7
- data/rust/src/selector.rs +2 -7
- data/rust/src/syntax.rs +41 -21
- data/rust/src/yerbafile.rs +86 -19
- metadata +12 -2
- data/rust/Cargo.lock +0 -805
- data/rust/src/document.rs +0 -2304
|
@@ -0,0 +1,784 @@
|
|
|
1
|
+
mod condition;
|
|
2
|
+
mod delete;
|
|
3
|
+
mod get;
|
|
4
|
+
mod insert;
|
|
5
|
+
mod set;
|
|
6
|
+
mod sort;
|
|
7
|
+
mod style;
|
|
8
|
+
|
|
9
|
+
use std::fs;
|
|
10
|
+
use std::path::{Path, PathBuf};
|
|
11
|
+
|
|
12
|
+
use rowan::ast::AstNode;
|
|
13
|
+
use rowan::TextRange;
|
|
14
|
+
|
|
15
|
+
use yaml_parser::ast::{BlockMap, BlockSeq, Root};
|
|
16
|
+
use yaml_parser::{SyntaxKind, SyntaxNode, SyntaxToken};
|
|
17
|
+
|
|
18
|
+
use crate::error::YerbaError;
|
|
19
|
+
use crate::QuoteStyle;
|
|
20
|
+
|
|
21
|
+
use crate::syntax::{
|
|
22
|
+
extract_scalar, extract_scalar_text, find_entry_by_key, find_scalar_token, format_scalar_value, is_map_key, is_yaml_non_string, preceding_whitespace_indent,
|
|
23
|
+
removal_range, unescape_double_quoted, unescape_single_quoted, ScalarValue,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
#[derive(Debug, Clone)]
|
|
27
|
+
pub struct SortField {
|
|
28
|
+
pub path: String,
|
|
29
|
+
pub ascending: bool,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl SortField {
|
|
33
|
+
pub fn asc(path: &str) -> Self {
|
|
34
|
+
SortField {
|
|
35
|
+
path: path.to_string(),
|
|
36
|
+
ascending: true,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn desc(path: &str) -> Self {
|
|
41
|
+
SortField {
|
|
42
|
+
path: path.to_string(),
|
|
43
|
+
ascending: false,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub fn parse(input: &str) -> Self {
|
|
48
|
+
if let Some((path, direction)) = input.rsplit_once(':') {
|
|
49
|
+
match direction {
|
|
50
|
+
"desc" | "descending" => SortField::desc(path),
|
|
51
|
+
_ => SortField::asc(input),
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
SortField::asc(input)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn parse_list(input: &str) -> Vec<Self> {
|
|
59
|
+
input.split(',').map(|field| SortField::parse(field.trim())).collect()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[repr(C)]
|
|
64
|
+
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
65
|
+
pub enum NodeType {
|
|
66
|
+
Scalar = 0,
|
|
67
|
+
Map = 1,
|
|
68
|
+
Sequence = 2,
|
|
69
|
+
NotFound = 3,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[derive(Debug, Clone, Copy, Default)]
|
|
73
|
+
pub struct Location {
|
|
74
|
+
pub start_line: usize,
|
|
75
|
+
pub start_column: usize,
|
|
76
|
+
pub end_line: usize,
|
|
77
|
+
pub end_column: usize,
|
|
78
|
+
pub start_offset: usize,
|
|
79
|
+
pub end_offset: usize,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[derive(Debug)]
|
|
83
|
+
pub struct NodeInfo {
|
|
84
|
+
pub node_type: NodeType,
|
|
85
|
+
pub is_list: bool,
|
|
86
|
+
pub value: Option<ScalarValue>,
|
|
87
|
+
pub list_values: Vec<ScalarValue>,
|
|
88
|
+
pub location: Location,
|
|
89
|
+
pub key_name: Option<String>,
|
|
90
|
+
pub key_location: Location,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[derive(Debug, Clone)]
|
|
94
|
+
pub enum InsertPosition {
|
|
95
|
+
At(usize),
|
|
96
|
+
Last,
|
|
97
|
+
Before(String),
|
|
98
|
+
After(String),
|
|
99
|
+
BeforeCondition(String),
|
|
100
|
+
AfterCondition(String),
|
|
101
|
+
FromSortOrder(Vec<String>),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[derive(Debug)]
|
|
105
|
+
pub struct Document {
|
|
106
|
+
root: SyntaxNode,
|
|
107
|
+
path: Option<PathBuf>,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl Document {
|
|
111
|
+
pub fn parse(source: &str) -> Result<Self, YerbaError> {
|
|
112
|
+
let tree = yaml_parser::parse(source).map_err(|error| YerbaError::ParseError(format!("{}", error)))?;
|
|
113
|
+
|
|
114
|
+
check_duplicate_keys(&tree)?;
|
|
115
|
+
|
|
116
|
+
Ok(Document { root: tree, path: None })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub fn parse_file(path: impl AsRef<Path>) -> Result<Self, YerbaError> {
|
|
120
|
+
let path = path.as_ref();
|
|
121
|
+
let source = fs::read_to_string(path)?;
|
|
122
|
+
|
|
123
|
+
let mut document = Self::parse(&source)?;
|
|
124
|
+
|
|
125
|
+
document.path = Some(path.to_path_buf());
|
|
126
|
+
|
|
127
|
+
Ok(document)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pub fn save(&self) -> Result<(), YerbaError> {
|
|
131
|
+
let path = self
|
|
132
|
+
.path
|
|
133
|
+
.as_ref()
|
|
134
|
+
.ok_or_else(|| YerbaError::IoError(std::io::Error::new(std::io::ErrorKind::NotFound, "no file path associated with this document")))?;
|
|
135
|
+
|
|
136
|
+
fs::write(path, self.to_string())?;
|
|
137
|
+
|
|
138
|
+
Ok(())
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pub fn save_to(&self, path: impl AsRef<Path>) -> Result<(), YerbaError> {
|
|
142
|
+
fs::write(path, self.to_string())?;
|
|
143
|
+
|
|
144
|
+
Ok(())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
pub fn navigate(&self, dot_path: &str) -> Result<SyntaxNode, YerbaError> {
|
|
148
|
+
Self::validate_path(dot_path)?;
|
|
149
|
+
|
|
150
|
+
if dot_path.is_empty() {
|
|
151
|
+
let root = Root::cast(self.root.clone()).ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
|
|
152
|
+
|
|
153
|
+
let document = root.documents().next().ok_or_else(|| YerbaError::SelectorNotFound(dot_path.to_string()))?;
|
|
154
|
+
|
|
155
|
+
return Ok(document.syntax().clone());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let nodes = self.navigate_all(dot_path);
|
|
159
|
+
|
|
160
|
+
match nodes.len() {
|
|
161
|
+
0 => Err(YerbaError::SelectorNotFound(dot_path.to_string())),
|
|
162
|
+
1 => Ok(nodes.into_iter().next().unwrap()),
|
|
163
|
+
_ => Err(YerbaError::AmbiguousSelector(dot_path.to_string(), nodes.len())),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pub fn navigate_all(&self, dot_path: &str) -> Vec<SyntaxNode> {
|
|
168
|
+
if Document::validate_path(dot_path).is_err() {
|
|
169
|
+
return Vec::new();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let parsed = crate::selector::Selector::parse(dot_path);
|
|
173
|
+
|
|
174
|
+
let root = match Root::cast(self.root.clone()) {
|
|
175
|
+
Some(root) => root,
|
|
176
|
+
None => return Vec::new(),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let document = match root.documents().next() {
|
|
180
|
+
Some(document) => document,
|
|
181
|
+
None => return Vec::new(),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
let mut current_nodes = vec![document.syntax().clone()];
|
|
185
|
+
|
|
186
|
+
if parsed.is_empty() {
|
|
187
|
+
if let Some(sequence) = document.syntax().descendants().find_map(BlockSeq::cast) {
|
|
188
|
+
current_nodes = sequence.entries().map(|entry| entry.syntax().clone()).collect();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return current_nodes;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for segment in parsed.segments() {
|
|
195
|
+
let mut next_nodes = Vec::new();
|
|
196
|
+
|
|
197
|
+
for node in ¤t_nodes {
|
|
198
|
+
next_nodes.extend(resolve_segment(node, segment));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
current_nodes = next_nodes;
|
|
202
|
+
|
|
203
|
+
if current_nodes.is_empty() {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
current_nodes
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
pub fn validate_path(dot_path: &str) -> Result<(), YerbaError> {
|
|
212
|
+
if dot_path.ends_with('.') {
|
|
213
|
+
return Err(YerbaError::ParseError(format!("invalid path: trailing dot in '{}'", dot_path)));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if dot_path.contains("..") {
|
|
217
|
+
return Err(YerbaError::ParseError(format!("invalid path: double dot in '{}'", dot_path)));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if dot_path.starts_with('.') {
|
|
221
|
+
return Err(YerbaError::ParseError(format!("invalid path: leading dot in '{}'", dot_path)));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if dot_path.contains('[') && !dot_path.contains(']') {
|
|
225
|
+
return Err(YerbaError::ParseError(format!("invalid path: unclosed bracket in '{}'", dot_path)));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
Ok(())
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn replace_token(&mut self, token: &SyntaxToken, new_text: &str) -> Result<(), YerbaError> {
|
|
232
|
+
let range = token.text_range();
|
|
233
|
+
|
|
234
|
+
self.apply_edit(range, new_text)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fn insert_after_node(&mut self, node: &SyntaxNode, text: &str) -> Result<(), YerbaError> {
|
|
238
|
+
let position = node.text_range().end();
|
|
239
|
+
let range = TextRange::new(position, position);
|
|
240
|
+
|
|
241
|
+
self.apply_edit(range, text)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
fn remove_node(&mut self, node: &SyntaxNode) -> Result<(), YerbaError> {
|
|
245
|
+
let inline_comment = self.find_inline_comment(node);
|
|
246
|
+
let range = removal_range(node);
|
|
247
|
+
|
|
248
|
+
if let Some((comment_text, comment_end)) = inline_comment {
|
|
249
|
+
let indent = preceding_whitespace_indent(node);
|
|
250
|
+
let replacement = format!("\n{}{}", indent, comment_text);
|
|
251
|
+
let expanded_range = TextRange::new(range.start(), comment_end);
|
|
252
|
+
|
|
253
|
+
self.apply_edit(expanded_range, &replacement)
|
|
254
|
+
} else {
|
|
255
|
+
self.apply_edit(range, "")
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fn find_inline_comment(&self, node: &SyntaxNode) -> Option<(String, rowan::TextSize)> {
|
|
260
|
+
let mut sibling = node.next_sibling_or_token();
|
|
261
|
+
|
|
262
|
+
while let Some(ref element) = sibling {
|
|
263
|
+
match element {
|
|
264
|
+
rowan::NodeOrToken::Token(token) => {
|
|
265
|
+
if token.kind() == SyntaxKind::COMMENT {
|
|
266
|
+
return Some((token.text().to_string(), token.text_range().end()));
|
|
267
|
+
} else if token.kind() == SyntaxKind::WHITESPACE {
|
|
268
|
+
if token.text().contains('\n') {
|
|
269
|
+
return None;
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
return None;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
_ => return None,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
sibling = match element {
|
|
279
|
+
rowan::NodeOrToken::Token(token) => token.next_sibling_or_token(),
|
|
280
|
+
rowan::NodeOrToken::Node(node) => node.next_sibling_or_token(),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
None
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn reorder_entries<T>(&mut self, parent: &SyntaxNode, entries: &[T], from: usize, to: usize) -> Result<(), YerbaError>
|
|
288
|
+
where
|
|
289
|
+
T: rowan::ast::AstNode<Language = yaml_parser::YamlLanguage>,
|
|
290
|
+
{
|
|
291
|
+
let length = entries.len();
|
|
292
|
+
|
|
293
|
+
if from >= length {
|
|
294
|
+
return Err(YerbaError::IndexOutOfBounds(from, length));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if to >= length {
|
|
298
|
+
return Err(YerbaError::IndexOutOfBounds(to, length));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let (groups, range) = collect_groups_with_range(parent);
|
|
302
|
+
|
|
303
|
+
let mut reordered = groups.clone();
|
|
304
|
+
let item = reordered.remove(from);
|
|
305
|
+
reordered.insert(to, item);
|
|
306
|
+
|
|
307
|
+
let indent = entries.get(1).map(|entry| preceding_whitespace_indent(entry.syntax())).unwrap_or_default();
|
|
308
|
+
|
|
309
|
+
let text = rebuild_from_groups(&reordered, &indent, true);
|
|
310
|
+
|
|
311
|
+
self.apply_edit(range, &text)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fn apply_edit(&mut self, range: TextRange, replacement: &str) -> Result<(), YerbaError> {
|
|
315
|
+
let source = self.root.text().to_string();
|
|
316
|
+
let start: usize = range.start().into();
|
|
317
|
+
let end: usize = range.end().into();
|
|
318
|
+
|
|
319
|
+
let mut new_source = source;
|
|
320
|
+
new_source.replace_range(start..end, replacement);
|
|
321
|
+
|
|
322
|
+
let path = self.path.take();
|
|
323
|
+
*self = Self::parse(&new_source)?;
|
|
324
|
+
self.path = path;
|
|
325
|
+
|
|
326
|
+
Ok(())
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
impl std::fmt::Display for Document {
|
|
331
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
332
|
+
write!(f, "{}", self.root.text())
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
fn check_duplicate_keys(root: &SyntaxNode) -> Result<(), YerbaError> {
|
|
337
|
+
for node in root.descendants() {
|
|
338
|
+
if let Some(map) = BlockMap::cast(node) {
|
|
339
|
+
let mut seen: std::collections::HashMap<String, rowan::TextSize> = std::collections::HashMap::new();
|
|
340
|
+
|
|
341
|
+
for entry in map.entries() {
|
|
342
|
+
if let Some(key) = entry.key() {
|
|
343
|
+
if let Some(key_text) = extract_scalar_text(key.syntax()) {
|
|
344
|
+
let offset = key.syntax().text_range().start();
|
|
345
|
+
|
|
346
|
+
if let Some(&first_offset) = seen.get(&key_text) {
|
|
347
|
+
let source = root.text().to_string();
|
|
348
|
+
let first_line = source[..first_offset.into()].matches('\n').count() + 1;
|
|
349
|
+
let duplicate_line = source[..usize::from(offset)].matches('\n').count() + 1;
|
|
350
|
+
let line_content = source.lines().nth(duplicate_line - 1).unwrap_or("").to_string();
|
|
351
|
+
|
|
352
|
+
return Err(YerbaError::DuplicateKey {
|
|
353
|
+
key: key_text,
|
|
354
|
+
first_line,
|
|
355
|
+
duplicate_line,
|
|
356
|
+
line_content,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
seen.insert(key_text, offset);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
Ok(())
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
pub(crate) fn compute_location(source: &str, start_offset: usize, end_offset: usize) -> Location {
|
|
371
|
+
let start = start_offset.min(source.len());
|
|
372
|
+
let end = end_offset.min(source.len());
|
|
373
|
+
|
|
374
|
+
let before_start = &source[..start];
|
|
375
|
+
let start_line = before_start.chars().filter(|c| *c == '\n').count() + 1;
|
|
376
|
+
let start_column = start - before_start.rfind('\n').map(|p| p + 1).unwrap_or(0);
|
|
377
|
+
|
|
378
|
+
let before_end = &source[..end];
|
|
379
|
+
let end_line = before_end.chars().filter(|c| *c == '\n').count() + 1;
|
|
380
|
+
let end_column = end - before_end.rfind('\n').map(|p| p + 1).unwrap_or(0);
|
|
381
|
+
|
|
382
|
+
Location {
|
|
383
|
+
start_offset: start,
|
|
384
|
+
end_offset: end,
|
|
385
|
+
start_line,
|
|
386
|
+
start_column,
|
|
387
|
+
end_line,
|
|
388
|
+
end_column,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
pub fn collect_selectors(value: &serde_yaml::Value, prefix: &str, selectors: &mut Vec<String>) {
|
|
393
|
+
match value {
|
|
394
|
+
serde_yaml::Value::Mapping(map) => {
|
|
395
|
+
for (key, child) in map {
|
|
396
|
+
if let serde_yaml::Value::String(key_string) = key {
|
|
397
|
+
let selector = if prefix.is_empty() {
|
|
398
|
+
key_string.clone()
|
|
399
|
+
} else {
|
|
400
|
+
format!("{}.{}", prefix, key_string)
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
selectors.push(selector.clone());
|
|
404
|
+
collect_selectors(child, &selector, selectors);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
serde_yaml::Value::Sequence(sequence) => {
|
|
410
|
+
let bracket_prefix = format!("{}[]", prefix);
|
|
411
|
+
selectors.push(bracket_prefix.clone());
|
|
412
|
+
|
|
413
|
+
for item in sequence {
|
|
414
|
+
collect_selectors(item, &bracket_prefix, selectors);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
_ => {}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
pub(crate) fn node_to_yaml_value(node: &SyntaxNode) -> serde_yaml::Value {
|
|
423
|
+
if let Some(sequence) = node.descendants().find_map(BlockSeq::cast) {
|
|
424
|
+
let map_position = node.descendants().find_map(BlockMap::cast).map(|map| map.syntax().text_range().start());
|
|
425
|
+
|
|
426
|
+
let sequence_position = sequence.syntax().text_range().start();
|
|
427
|
+
|
|
428
|
+
if map_position.is_none() || sequence_position <= map_position.unwrap() {
|
|
429
|
+
let values: Vec<serde_yaml::Value> = sequence.entries().map(|entry| node_to_yaml_value(entry.syntax())).collect();
|
|
430
|
+
|
|
431
|
+
return serde_yaml::Value::Sequence(values);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if let Some(map) = node.descendants().find_map(BlockMap::cast) {
|
|
436
|
+
let mut mapping = serde_yaml::Mapping::new();
|
|
437
|
+
|
|
438
|
+
for entry in map.entries() {
|
|
439
|
+
let key = entry.key().and_then(|key_node| extract_scalar_text(key_node.syntax())).unwrap_or_default();
|
|
440
|
+
|
|
441
|
+
let value = entry
|
|
442
|
+
.value()
|
|
443
|
+
.map(|value_node| node_to_yaml_value(value_node.syntax()))
|
|
444
|
+
.unwrap_or(serde_yaml::Value::Null);
|
|
445
|
+
|
|
446
|
+
mapping.insert(serde_yaml::Value::String(key), value);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return serde_yaml::Value::Mapping(mapping);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if let Some(sequence) = node.descendants().find_map(BlockSeq::cast) {
|
|
453
|
+
let values: Vec<serde_yaml::Value> = sequence.entries().map(|entry| node_to_yaml_value(entry.syntax())).collect();
|
|
454
|
+
|
|
455
|
+
return serde_yaml::Value::Sequence(values);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if let Some(block_scalar) = node.descendants().find(|child| child.kind() == SyntaxKind::BLOCK_SCALAR) {
|
|
459
|
+
let text = block_scalar
|
|
460
|
+
.descendants_with_tokens()
|
|
461
|
+
.filter_map(|element| element.into_token())
|
|
462
|
+
.find(|token| token.kind() == SyntaxKind::BLOCK_SCALAR_TEXT)
|
|
463
|
+
.map(|token| token.text().to_string())
|
|
464
|
+
.unwrap_or_default();
|
|
465
|
+
|
|
466
|
+
return serde_yaml::Value::String(text);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if let Some(scalar) = extract_scalar(node) {
|
|
470
|
+
use crate::syntax::{detect_yaml_type, is_yaml_truthy, YerbaValueType};
|
|
471
|
+
|
|
472
|
+
return match detect_yaml_type(&scalar) {
|
|
473
|
+
YerbaValueType::Null => serde_yaml::Value::Null,
|
|
474
|
+
YerbaValueType::Boolean => serde_yaml::Value::Bool(is_yaml_truthy(&scalar.text)),
|
|
475
|
+
|
|
476
|
+
YerbaValueType::Integer => scalar
|
|
477
|
+
.text
|
|
478
|
+
.parse::<i64>()
|
|
479
|
+
.map(|n| serde_yaml::Value::Number(serde_yaml::Number::from(n)))
|
|
480
|
+
.unwrap_or(serde_yaml::Value::String(scalar.text)),
|
|
481
|
+
|
|
482
|
+
YerbaValueType::Float => scalar
|
|
483
|
+
.text
|
|
484
|
+
.parse::<f64>()
|
|
485
|
+
.map(|n| serde_yaml::Value::Number(serde_yaml::Number::from(n)))
|
|
486
|
+
.unwrap_or(serde_yaml::Value::String(scalar.text)),
|
|
487
|
+
|
|
488
|
+
YerbaValueType::String => serde_yaml::Value::String(scalar.text),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let text = node.text().to_string();
|
|
493
|
+
|
|
494
|
+
serde_yaml::from_str(&text).unwrap_or(serde_yaml::Value::String(text))
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
pub(crate) fn parse_condition(condition: &str) -> Option<(String, &str, String)> {
|
|
498
|
+
let (left, operator, right) = if let Some(index) = condition.find(" not_contains ") {
|
|
499
|
+
(condition[..index].trim(), "not_contains", condition[index + 14..].trim())
|
|
500
|
+
} else if let Some(index) = condition.find(" contains ") {
|
|
501
|
+
(condition[..index].trim(), "contains", condition[index + 10..].trim())
|
|
502
|
+
} else if let Some(index) = condition.find("!=") {
|
|
503
|
+
(condition[..index].trim(), "!=", condition[index + 2..].trim())
|
|
504
|
+
} else if let Some(index) = condition.find("==") {
|
|
505
|
+
(condition[..index].trim(), "==", condition[index + 2..].trim())
|
|
506
|
+
} else {
|
|
507
|
+
return None;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
let right = right
|
|
511
|
+
.trim_start_matches('"')
|
|
512
|
+
.trim_end_matches('"')
|
|
513
|
+
.trim_start_matches('\'')
|
|
514
|
+
.trim_end_matches('\'');
|
|
515
|
+
|
|
516
|
+
Some((left.to_string(), operator, right.to_string()))
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
fn resolve_segment(node: &SyntaxNode, segment: &crate::selector::SelectorSegment) -> Vec<SyntaxNode> {
|
|
520
|
+
use crate::selector::SelectorSegment;
|
|
521
|
+
|
|
522
|
+
match segment {
|
|
523
|
+
SelectorSegment::AllItems => {
|
|
524
|
+
if let Some(sequence) = node.descendants().find_map(BlockSeq::cast) {
|
|
525
|
+
sequence.entries().map(|entry| entry.syntax().clone()).collect()
|
|
526
|
+
} else {
|
|
527
|
+
Vec::new()
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
SelectorSegment::Index(index) => {
|
|
532
|
+
if let Some(sequence) = node.descendants().find_map(BlockSeq::cast) {
|
|
533
|
+
sequence.entries().nth(*index).map(|entry| vec![entry.syntax().clone()]).unwrap_or_default()
|
|
534
|
+
} else {
|
|
535
|
+
Vec::new()
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
SelectorSegment::Key(key) => {
|
|
540
|
+
if let Some(map) = node.descendants().find_map(BlockMap::cast) {
|
|
541
|
+
if let Some(entry) = find_entry_by_key(&map, key) {
|
|
542
|
+
if let Some(value) = entry.value() {
|
|
543
|
+
return vec![value.syntax().clone()];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
Vec::new()
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
pub(crate) fn navigate_from_node(node: &SyntaxNode, path: &str) -> Vec<SyntaxNode> {
|
|
554
|
+
let parsed = crate::selector::Selector::parse(path);
|
|
555
|
+
let mut current_nodes = vec![node.clone()];
|
|
556
|
+
|
|
557
|
+
for segment in parsed.segments() {
|
|
558
|
+
let mut next_nodes = Vec::new();
|
|
559
|
+
|
|
560
|
+
for current in ¤t_nodes {
|
|
561
|
+
next_nodes.extend(resolve_segment(current, segment));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
current_nodes = next_nodes;
|
|
565
|
+
|
|
566
|
+
if current_nodes.is_empty() {
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
current_nodes
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#[derive(Debug, Clone)]
|
|
575
|
+
pub(crate) struct EntryGroup {
|
|
576
|
+
pub(crate) separator: String,
|
|
577
|
+
pub(crate) preceding: String,
|
|
578
|
+
pub(crate) body: String,
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
impl EntryGroup {
|
|
582
|
+
pub(crate) fn full_text(&self) -> String {
|
|
583
|
+
if self.preceding.is_empty() {
|
|
584
|
+
self.body.clone()
|
|
585
|
+
} else {
|
|
586
|
+
format!("{}\n{}", self.preceding, self.body)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
pub(crate) fn collect_blank_line_edits(node: &SyntaxNode, blank_lines: usize, edits: &mut Vec<(TextRange, String)>) {
|
|
592
|
+
use crate::syntax::preceding_whitespace_token;
|
|
593
|
+
|
|
594
|
+
if let Some(whitespace_token) = preceding_whitespace_token(node) {
|
|
595
|
+
let whitespace_text = whitespace_token.text();
|
|
596
|
+
let newline_count = whitespace_text.chars().filter(|character| *character == '\n').count();
|
|
597
|
+
|
|
598
|
+
let indent = whitespace_text.rfind('\n').map(|position| &whitespace_text[position + 1..]).unwrap_or("");
|
|
599
|
+
|
|
600
|
+
let desired_newlines = blank_lines + 1;
|
|
601
|
+
|
|
602
|
+
if newline_count != desired_newlines {
|
|
603
|
+
let new_whitespace = format!("{}{}", "\n".repeat(desired_newlines), indent);
|
|
604
|
+
|
|
605
|
+
edits.push((whitespace_token.text_range(), new_whitespace));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
fn collect_entry_groups(parent: &SyntaxNode) -> Vec<EntryGroup> {
|
|
611
|
+
let mut groups: Vec<EntryGroup> = Vec::new();
|
|
612
|
+
let mut buffer = String::new();
|
|
613
|
+
|
|
614
|
+
for child in parent.children_with_tokens() {
|
|
615
|
+
let is_entry = child.as_node().is_some() && matches!(child.as_node().unwrap().kind(), SyntaxKind::BLOCK_MAP_ENTRY | SyntaxKind::BLOCK_SEQ_ENTRY);
|
|
616
|
+
|
|
617
|
+
if is_entry {
|
|
618
|
+
let entry_text = child.as_node().unwrap().text().to_string();
|
|
619
|
+
|
|
620
|
+
if groups.is_empty() {
|
|
621
|
+
let preceding = buffer.trim_start_matches('\n').to_string();
|
|
622
|
+
|
|
623
|
+
groups.push(EntryGroup {
|
|
624
|
+
separator: String::new(),
|
|
625
|
+
preceding,
|
|
626
|
+
body: entry_text,
|
|
627
|
+
});
|
|
628
|
+
} else {
|
|
629
|
+
let (trailing, separator, preceding) = split_at_blank_line(&buffer);
|
|
630
|
+
|
|
631
|
+
if let Some(last) = groups.last_mut() {
|
|
632
|
+
last.body.push_str(&trailing);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
groups.push(EntryGroup {
|
|
636
|
+
separator,
|
|
637
|
+
preceding,
|
|
638
|
+
body: entry_text,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
buffer.clear();
|
|
643
|
+
} else {
|
|
644
|
+
let text = match &child {
|
|
645
|
+
rowan::NodeOrToken::Node(node) => node.text().to_string(),
|
|
646
|
+
rowan::NodeOrToken::Token(token) => token.text().to_string(),
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
buffer.push_str(&text);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if let Some(last) = groups.last_mut() {
|
|
654
|
+
let trimmed = buffer.trim_end_matches(['\n', ' ', '\t']);
|
|
655
|
+
|
|
656
|
+
if !trimmed.is_empty() {
|
|
657
|
+
last.body.push_str(trimmed);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
for group in &mut groups {
|
|
662
|
+
let trimmed = group.body.trim_end_matches(['\n', ' ', '\t']);
|
|
663
|
+
|
|
664
|
+
group.body = trimmed.to_string();
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
groups
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
fn split_at_blank_line(text: &str) -> (String, String, String) {
|
|
671
|
+
if let Some(position) = text.find("\n\n") {
|
|
672
|
+
let trailing = text[..position].to_string();
|
|
673
|
+
let rest = &text[position..];
|
|
674
|
+
let content_start = rest.len() - rest.trim_start_matches('\n').len();
|
|
675
|
+
let separator = rest[..content_start].to_string();
|
|
676
|
+
let preceding = rest[content_start..].trim_end_matches(['\n', ' ', '\t']).to_string();
|
|
677
|
+
|
|
678
|
+
(trailing, separator, preceding)
|
|
679
|
+
} else {
|
|
680
|
+
(text.to_string(), String::new(), String::new())
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
pub(crate) fn collect_preceding_sibling_comments(parent: &SyntaxNode) -> (String, Option<rowan::TextSize>) {
|
|
685
|
+
let mut comments: Vec<String> = Vec::new();
|
|
686
|
+
let mut earliest_start = None;
|
|
687
|
+
let mut node = parent.clone();
|
|
688
|
+
|
|
689
|
+
loop {
|
|
690
|
+
let mut sibling = node.prev_sibling_or_token();
|
|
691
|
+
|
|
692
|
+
while let Some(ref element) = sibling {
|
|
693
|
+
match element {
|
|
694
|
+
rowan::NodeOrToken::Token(token) => {
|
|
695
|
+
if token.kind() == SyntaxKind::COMMENT {
|
|
696
|
+
comments.push(token.text().to_string());
|
|
697
|
+
earliest_start = Some(token.text_range().start());
|
|
698
|
+
} else if token.kind() == SyntaxKind::WHITESPACE {
|
|
699
|
+
// Keep looking past whitespace
|
|
700
|
+
} else {
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
_ => break,
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
sibling = match element {
|
|
708
|
+
rowan::NodeOrToken::Token(token) => token.prev_sibling_or_token(),
|
|
709
|
+
rowan::NodeOrToken::Node(node) => node.prev_sibling_or_token(),
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
if !comments.is_empty() {
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
match node.parent() {
|
|
718
|
+
Some(parent) if parent.kind() == SyntaxKind::BLOCK || parent.kind() == SyntaxKind::DOCUMENT || parent.kind() == SyntaxKind::BLOCK_MAP_VALUE => {
|
|
719
|
+
node = parent
|
|
720
|
+
}
|
|
721
|
+
_ => break,
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
comments.reverse();
|
|
726
|
+
(comments.join("\n"), earliest_start)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
pub(crate) fn collect_groups_with_range(parent: &SyntaxNode) -> (Vec<EntryGroup>, TextRange) {
|
|
730
|
+
let mut groups = collect_entry_groups(parent);
|
|
731
|
+
|
|
732
|
+
let (sibling_comments, earliest_start) = collect_preceding_sibling_comments(parent);
|
|
733
|
+
|
|
734
|
+
if !sibling_comments.is_empty() {
|
|
735
|
+
if let Some(first) = groups.first_mut() {
|
|
736
|
+
if first.preceding.is_empty() {
|
|
737
|
+
first.preceding = sibling_comments;
|
|
738
|
+
} else {
|
|
739
|
+
first.preceding = format!("{}\n{}", sibling_comments, first.preceding);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
let range = match earliest_start {
|
|
745
|
+
Some(start) => TextRange::new(start, parent.text_range().end()),
|
|
746
|
+
None => parent.text_range(),
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
(groups, range)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
pub(crate) fn rebuild_from_groups(groups: &[EntryGroup], indent: &str, preserve_separators: bool) -> String {
|
|
753
|
+
let default_separator = if preserve_separators {
|
|
754
|
+
groups
|
|
755
|
+
.iter()
|
|
756
|
+
.find(|group| !group.separator.is_empty())
|
|
757
|
+
.map(|group| group.separator.clone())
|
|
758
|
+
.unwrap_or_else(|| "\n".to_string())
|
|
759
|
+
} else {
|
|
760
|
+
"\n".to_string()
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
groups
|
|
764
|
+
.iter()
|
|
765
|
+
.enumerate()
|
|
766
|
+
.map(|(index, group)| {
|
|
767
|
+
if index == 0 {
|
|
768
|
+
group.full_text()
|
|
769
|
+
} else {
|
|
770
|
+
let separator = if preserve_separators && !group.separator.is_empty() {
|
|
771
|
+
&group.separator
|
|
772
|
+
} else {
|
|
773
|
+
&default_separator
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
if group.preceding.is_empty() {
|
|
777
|
+
format!("{}{}{}", separator, indent, group.body)
|
|
778
|
+
} else {
|
|
779
|
+
format!("{}{}\n{}{}", separator, group.preceding, indent, group.body)
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
})
|
|
783
|
+
.collect()
|
|
784
|
+
}
|