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                range_to_remove.start = prev_sibling.start_byte();
 90            }
 91    }
 92
 93    // Include any leading whitespace/newline, including comments
 94    let text_before = &contents[..range_to_remove.start];
 95    if let Some(last_newline) = text_before.rfind('\n') {
 96        let whitespace_start = last_newline + 1;
 97        let potential_whitespace = &contents[whitespace_start..range_to_remove.start];
 98
 99        // Check if it's only whitespace or comments
100        let mut is_whitespace_or_comment = true;
101        let mut in_comment = false;
102        let mut chars = potential_whitespace.chars().peekable();
103
104        while let Some(ch) = chars.next() {
105            if in_comment {
106                if ch == '\n' {
107                    in_comment = false;
108                }
109            } else if ch == '/' && chars.peek() == Some(&'/') {
110                in_comment = true;
111                chars.next(); // Skip the second '/'
112            } else if !ch.is_whitespace() {
113                is_whitespace_or_comment = false;
114                break;
115            }
116        }
117
118        if is_whitespace_or_comment {
119            range_to_remove.start = whitespace_start;
120        }
121    }
122
123    // Also check if we need to include trailing whitespace up to the next line
124    let text_after = &contents[range_to_remove.end..];
125    if let Some(newline_pos) = text_after.find('\n')
126        && text_after[..newline_pos].chars().all(|c| c.is_whitespace()) {
127            range_to_remove.end += newline_pos + 1;
128        }
129
130    Some((range_to_remove, String::new()))
131}