1use std::ops::Range;
2use tree_sitter::{Query, QueryMatch};
3
4use crate::MigrationPatterns;
5
6pub const SETTINGS_PATTERNS: MigrationPatterns = &[
7 (SETTINGS_VERSION_PATTERN, remove_version_fields),
8 (
9 SETTINGS_NESTED_VERSION_PATTERN,
10 remove_nested_version_fields,
11 ),
12];
13
14const SETTINGS_VERSION_PATTERN: &str = r#"(document
15 (object
16 (pair
17 key: (string (string_content) @key)
18 value: (object
19 (pair
20 key: (string (string_content) @version_key)
21 value: (_) @version_value
22 ) @version_pair
23 )
24 )
25 )
26 (#eq? @key "agent")
27 (#eq? @version_key "version")
28)"#;
29
30const SETTINGS_NESTED_VERSION_PATTERN: &str = r#"(document
31 (object
32 (pair
33 key: (string (string_content) @language_models)
34 value: (object
35 (pair
36 key: (string (string_content) @provider)
37 value: (object
38 (pair
39 key: (string (string_content) @version_key)
40 value: (_) @version_value
41 ) @version_pair
42 )
43 )
44 )
45 )
46 )
47 (#eq? @language_models "language_models")
48 (#match? @provider "^(anthropic|openai)$")
49 (#eq? @version_key "version")
50)"#;
51
52fn remove_version_fields(
53 contents: &str,
54 mat: &QueryMatch,
55 query: &Query,
56) -> Option<(Range<usize>, String)> {
57 let version_pair_ix = query.capture_index_for_name("version_pair")?;
58 let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?;
59
60 remove_pair_with_whitespace(contents, version_pair_node)
61}
62
63fn remove_nested_version_fields(
64 contents: &str,
65 mat: &QueryMatch,
66 query: &Query,
67) -> Option<(Range<usize>, String)> {
68 let version_pair_ix = query.capture_index_for_name("version_pair")?;
69 let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?;
70
71 remove_pair_with_whitespace(contents, version_pair_node)
72}
73
74fn remove_pair_with_whitespace(
75 contents: &str,
76 pair_node: tree_sitter::Node,
77) -> Option<(Range<usize>, String)> {
78 let mut range_to_remove = pair_node.byte_range();
79
80 // Check if there's a comma after this pair
81 if let Some(next_sibling) = pair_node.next_sibling() {
82 if next_sibling.kind() == "," {
83 range_to_remove.end = next_sibling.end_byte();
84 }
85 } else {
86 // If no next sibling, check if there's a comma before
87 if let Some(prev_sibling) = pair_node.prev_sibling()
88 && prev_sibling.kind() == ","
89 {
90 range_to_remove.start = prev_sibling.start_byte();
91 }
92 }
93
94 // Include any leading whitespace/newline, including comments
95 let text_before = &contents[..range_to_remove.start];
96 if let Some(last_newline) = text_before.rfind('\n') {
97 let whitespace_start = last_newline + 1;
98 let potential_whitespace = &contents[whitespace_start..range_to_remove.start];
99
100 // Check if it's only whitespace or comments
101 let mut is_whitespace_or_comment = true;
102 let mut in_comment = false;
103 let mut chars = potential_whitespace.chars().peekable();
104
105 while let Some(ch) = chars.next() {
106 if in_comment {
107 if ch == '\n' {
108 in_comment = false;
109 }
110 } else if ch == '/' && chars.peek() == Some(&'/') {
111 in_comment = true;
112 chars.next(); // Skip the second '/'
113 } else if !ch.is_whitespace() {
114 is_whitespace_or_comment = false;
115 break;
116 }
117 }
118
119 if is_whitespace_or_comment {
120 range_to_remove.start = whitespace_start;
121 }
122 }
123
124 // Also check if we need to include trailing whitespace up to the next line
125 let text_after = &contents[range_to_remove.end..];
126 if let Some(newline_pos) = text_after.find('\n')
127 && text_after[..newline_pos].chars().all(|c| c.is_whitespace())
128 {
129 range_to_remove.end += newline_pos + 1;
130 }
131
132 Some((range_to_remove, String::new()))
133}