1use std::ops::Range;
2use tree_sitter::{Query, QueryMatch};
3
4use crate::MigrationPatterns;
5
6pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
7 SETTINGS_CONTEXT_SERVER_PATTERN,
8 flatten_context_server_command,
9)];
10
11const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document
12 (object
13 (pair
14 key: (string (string_content) @context-servers)
15 value: (object
16 (pair
17 key: (string (string_content) @server-name)
18 value: (object
19 (pair
20 key: (string (string_content) @source-key)
21 value: (string (string_content) @source-value)
22 )
23 (pair
24 key: (string (string_content) @command-key)
25 value: (object) @command-object
26 ) @command-pair
27 ) @server-settings
28 )
29 )
30 )
31 )
32 (#eq? @context-servers "context_servers")
33 (#eq? @source-key "source")
34 (#eq? @source-value "custom")
35 (#eq? @command-key "command")
36)"#;
37
38fn flatten_context_server_command(
39 contents: &str,
40 mat: &QueryMatch,
41 query: &Query,
42) -> Option<(Range<usize>, String)> {
43 let command_pair_index = query.capture_index_for_name("command-pair")?;
44 let command_pair = mat.nodes_for_capture_index(command_pair_index).next()?;
45
46 let command_object_index = query.capture_index_for_name("command-object")?;
47 let command_object = mat.nodes_for_capture_index(command_object_index).next()?;
48
49 let server_settings_index = query.capture_index_for_name("server-settings")?;
50 let _server_settings = mat.nodes_for_capture_index(server_settings_index).next()?;
51
52 // Parse the command object to extract path, args, and env
53 let mut path_value = None;
54 let mut args_value = None;
55 let mut env_value = None;
56
57 let mut cursor = command_object.walk();
58 for child in command_object.children(&mut cursor) {
59 if child.kind() == "pair"
60 && let Some(key_node) = child.child_by_field_name("key")
61 && let Some(string_content) = key_node.child(1)
62 {
63 let key = &contents[string_content.byte_range()];
64 if let Some(value_node) = child.child_by_field_name("value") {
65 let value_range = value_node.byte_range();
66 match key {
67 "path" => path_value = Some(&contents[value_range]),
68 "args" => args_value = Some(&contents[value_range]),
69 "env" => env_value = Some(&contents[value_range]),
70 _ => {}
71 }
72 }
73 }
74 }
75
76 let path = path_value?;
77
78 // Get the proper indentation from the command pair
79 let command_pair_start = command_pair.start_byte();
80 let line_start = contents[..command_pair_start]
81 .rfind('\n')
82 .map(|pos| pos + 1)
83 .unwrap_or(0);
84 let indent = &contents[line_start..command_pair_start];
85
86 // Build the replacement string
87 let mut replacement = format!("\"command\": {}", path);
88
89 // Add args if present - need to reduce indentation
90 if let Some(args) = args_value {
91 replacement.push_str(",\n");
92 replacement.push_str(indent);
93 replacement.push_str("\"args\": ");
94 let reduced_args = reduce_indentation(args, 4);
95 replacement.push_str(&reduced_args);
96 }
97
98 // Add env if present - need to reduce indentation
99 if let Some(env) = env_value {
100 replacement.push_str(",\n");
101 replacement.push_str(indent);
102 replacement.push_str("\"env\": ");
103 replacement.push_str(&reduce_indentation(env, 4));
104 }
105
106 let range_to_replace = command_pair.byte_range();
107 Some((range_to_replace, replacement))
108}
109
110fn reduce_indentation(text: &str, spaces: usize) -> String {
111 let lines: Vec<&str> = text.lines().collect();
112 let mut result = String::new();
113
114 for (i, line) in lines.iter().enumerate() {
115 if i > 0 {
116 result.push('\n');
117 }
118
119 // Count leading spaces
120 let leading_spaces = line.chars().take_while(|&c| c == ' ').count();
121
122 if leading_spaces >= spaces {
123 // Reduce indentation
124 result.push_str(&line[spaces..]);
125 } else {
126 // Keep line as is if it doesn't have enough indentation
127 result.push_str(line);
128 }
129 }
130
131 result
132}