settings.rs

  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}