Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/migrator/src/migrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ pub(crate) mod m_2025_06_16 {
pub(crate) use settings::SETTINGS_PATTERNS;
}

pub(crate) mod m_2025_06_25 {
mod settings;

pub(crate) use settings::SETTINGS_PATTERNS;
}

pub(crate) mod m_2025_06_27 {
mod settings;

Expand Down
133 changes: 133 additions & 0 deletions crates/migrator/src/migrations/m_2025_06_25/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};

use crate::MigrationPatterns;

pub const SETTINGS_PATTERNS: MigrationPatterns = &[
(SETTINGS_VERSION_PATTERN, remove_version_fields),
(
SETTINGS_NESTED_VERSION_PATTERN,
remove_nested_version_fields,
),
];

const SETTINGS_VERSION_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @key)
value: (object
(pair
key: (string (string_content) @version_key)
value: (_) @version_value
) @version_pair
)
)
)
(#eq? @key "agent")
(#eq? @version_key "version")
)"#;

const SETTINGS_NESTED_VERSION_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @language_models)
value: (object
(pair
key: (string (string_content) @provider)
value: (object
(pair
key: (string (string_content) @version_key)
value: (_) @version_value
) @version_pair
)
)
)
)
)
(#eq? @language_models "language_models")
(#match? @provider "^(anthropic|openai)$")
(#eq? @version_key "version")
)"#;

fn remove_version_fields(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let version_pair_ix = query.capture_index_for_name("version_pair")?;
let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?;

remove_pair_with_whitespace(contents, version_pair_node)
}

fn remove_nested_version_fields(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let version_pair_ix = query.capture_index_for_name("version_pair")?;
let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?;

remove_pair_with_whitespace(contents, version_pair_node)
}

fn remove_pair_with_whitespace(
contents: &str,
pair_node: tree_sitter::Node,
) -> Option<(Range<usize>, String)> {
let mut range_to_remove = pair_node.byte_range();

// Check if there's a comma after this pair
if let Some(next_sibling) = pair_node.next_sibling() {
if next_sibling.kind() == "," {
range_to_remove.end = next_sibling.end_byte();
}
} else {
// If no next sibling, check if there's a comma before
if let Some(prev_sibling) = pair_node.prev_sibling() {
if prev_sibling.kind() == "," {
range_to_remove.start = prev_sibling.start_byte();
}
}
}

// Include any leading whitespace/newline, including comments
let text_before = &contents[..range_to_remove.start];
if let Some(last_newline) = text_before.rfind('\n') {
let whitespace_start = last_newline + 1;
let potential_whitespace = &contents[whitespace_start..range_to_remove.start];

// Check if it's only whitespace or comments
let mut is_whitespace_or_comment = true;
let mut in_comment = false;
let mut chars = potential_whitespace.chars().peekable();

while let Some(ch) = chars.next() {
if in_comment {
if ch == '\n' {
in_comment = false;
}
} else if ch == '/' && chars.peek() == Some(&'/') {
in_comment = true;
chars.next(); // Skip the second '/'
} else if !ch.is_whitespace() {
is_whitespace_or_comment = false;
break;
}
}

if is_whitespace_or_comment {
range_to_remove.start = whitespace_start;
}
}

// Also check if we need to include trailing whitespace up to the next line
let text_after = &contents[range_to_remove.end..];
if let Some(newline_pos) = text_after.find('\n') {
if text_after[..newline_pos].chars().all(|c| c.is_whitespace()) {
range_to_remove.end += newline_pos + 1;
}
}

Some((range_to_remove, String::new()))
}
79 changes: 79 additions & 0 deletions crates/migrator/src/migrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_06_16::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_06_16,
),
(
migrations::m_2025_06_25::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_06_25,
),
(
migrations::m_2025_06_27::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_06_27,
Expand Down Expand Up @@ -258,6 +262,10 @@ define_query!(
SETTINGS_QUERY_2025_06_16,
migrations::m_2025_06_16::SETTINGS_PATTERNS
);
define_query!(
SETTINGS_QUERY_2025_06_25,
migrations::m_2025_06_25::SETTINGS_PATTERNS
);
define_query!(
SETTINGS_QUERY_2025_06_27,
migrations::m_2025_06_27::SETTINGS_PATTERNS
Expand Down Expand Up @@ -1081,6 +1089,77 @@ mod tests {
);
}

#[test]
fn test_remove_version_fields() {
assert_migrate_settings(
r#"{
"language_models": {
"anthropic": {
"version": "1",
"api_url": "https://api.anthropic.com"
},
"openai": {
"version": "1",
"api_url": "https://api.openai.com/v1"
}
},
"agent": {
"version": "2",
"enabled": true,
"preferred_completion_mode": "normal",
"button": true,
"dock": "right",
"default_width": 640,
"default_height": 320,
"default_model": {
"provider": "zed.dev",
"model": "claude-sonnet-4"
}
}
}"#,
Some(
r#"{
"language_models": {
"anthropic": {
"api_url": "https://api.anthropic.com"
},
"openai": {
"api_url": "https://api.openai.com/v1"
}
},
"agent": {
"enabled": true,
"preferred_completion_mode": "normal",
"button": true,
"dock": "right",
"default_width": 640,
"default_height": 320,
"default_model": {
"provider": "zed.dev",
"model": "claude-sonnet-4"
}
}
}"#,
),
);

// Test that version fields in other contexts are not removed
assert_migrate_settings(
r#"{
"language_models": {
"other_provider": {
"version": "1",
"api_url": "https://api.example.com"
}
},
"other_section": {
"version": "1"
}
}"#,
None,
);
}

#[test]
fn test_flatten_context_server_command() {
assert_migrate_settings(
Expand Down