logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled

Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree
with a 3-file patch applied (no upstream history).

Patch (crates/gpui_linux/src/linux/wayland/):
  - serial.rs: add SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; add latest_serial_of()
  - window.rs: activate() uses keyboard-enter serial (Mutter focus gate)

Mutter honors window activation only when the token carries the keyboard-
focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial.
See docs/tray-window-focus-wayland.md in logiguard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 02:22:17 +03:30
commit b72a46db68
3984 changed files with 1583326 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
use anyhow::Result;
use serde_json::Value;
use settings_content::{PlatformOverrides, ReleaseChannelOverrides};
/// Applies a migration callback to the root settings object as well as all
/// nested platform, release-channel, and profile override objects.
pub(crate) fn migrate_settings(
value: &mut Value,
migrate_one: &mut dyn FnMut(&mut serde_json::Map<String, Value>) -> Result<()>,
) -> Result<()> {
let Some(root_object) = value.as_object_mut() else {
return Ok(());
};
migrate_one(root_object)?;
let override_keys = ReleaseChannelOverrides::OVERRIDE_KEYS
.iter()
.copied()
.chain(PlatformOverrides::OVERRIDE_KEYS.iter().copied());
for key in override_keys {
if let Some(sub_object) = root_object.get_mut(key) {
if let Some(sub_map) = sub_object.as_object_mut() {
migrate_one(sub_map)?;
}
}
}
if let Some(profiles) = root_object.get_mut("profiles") {
if let Some(profiles_object) = profiles.as_object_mut() {
for profile_value in profiles_object.values_mut() {
if let Some(profile_map) = profile_value.as_object_mut() {
if let Some(inner) = profile_map
.get_mut("settings")
.and_then(|v| v.as_object_mut())
{
migrate_one(inner)?;
} else {
migrate_one(profile_map)?;
}
}
}
}
}
Ok(())
}
/// Applies a migration callback to a value and its `languages` children,
/// at the root level as well as all nested platform, release-channel, and
/// profile override objects.
pub(crate) fn migrate_language_setting(
value: &mut Value,
migrate_fn: fn(&mut Value, path: &[&str]) -> Result<()>,
) -> Result<()> {
fn apply_to_value_and_languages(
value: &mut Value,
prefix: &[&str],
migrate_fn: fn(&mut Value, path: &[&str]) -> Result<()>,
) -> Result<()> {
migrate_fn(value, prefix)?;
let languages = value
.as_object_mut()
.and_then(|obj| obj.get_mut("languages"))
.and_then(|languages| languages.as_object_mut());
if let Some(languages) = languages {
for (language_name, language) in languages.iter_mut() {
let mut path: Vec<&str> = prefix.to_vec();
path.push("languages");
path.push(language_name);
migrate_fn(language, &path)?;
}
}
Ok(())
}
if !value.is_object() {
return Ok(());
}
apply_to_value_and_languages(value, &[], migrate_fn)?;
let Some(root_object) = value.as_object_mut() else {
return Ok(());
};
let override_keys = ReleaseChannelOverrides::OVERRIDE_KEYS
.iter()
.copied()
.chain(PlatformOverrides::OVERRIDE_KEYS.iter().copied());
for key in override_keys {
if let Some(sub_value) = root_object.get_mut(key) {
apply_to_value_and_languages(sub_value, &[key], migrate_fn)?;
}
}
if let Some(profiles) = root_object.get_mut("profiles") {
if let Some(profiles_object) = profiles.as_object_mut() {
let profile_names: Vec<String> = profiles_object.keys().cloned().collect();
for profile_name in &profile_names {
if let Some(profile_value) = profiles_object.get_mut(profile_name.as_str()) {
if let Some(settings_value) = profile_value
.as_object_mut()
.and_then(|m| m.get_mut("settings"))
{
apply_to_value_and_languages(
settings_value,
&["profiles", profile_name],
migrate_fn,
)?;
} else {
apply_to_value_and_languages(
profile_value,
&["profiles", profile_name],
migrate_fn,
)?;
}
}
}
}
}
Ok(())
}
pub(crate) mod m_2025_01_02 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_01_29 {
mod keymap;
mod settings;
pub(crate) use keymap::KEYMAP_PATTERNS;
pub(crate) use settings::{SETTINGS_PATTERNS, replace_edit_prediction_provider_setting};
}
pub(crate) mod m_2025_01_30 {
mod keymap;
mod settings;
pub(crate) use keymap::KEYMAP_PATTERNS;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_03_03 {
mod keymap;
pub(crate) use keymap::KEYMAP_PATTERNS;
}
pub(crate) mod m_2025_03_06 {
mod keymap;
pub(crate) use keymap::KEYMAP_PATTERNS;
}
pub(crate) mod m_2025_03_29 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_04_15 {
mod keymap;
mod settings;
pub(crate) use keymap::KEYMAP_PATTERNS;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_04_21 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_04_23 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_05_05 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_05_08 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_06_16 {
mod settings;
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;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_07_08 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_10_01 {
mod settings;
pub(crate) use settings::flatten_code_actions_formatters;
}
pub(crate) mod m_2025_10_02 {
mod settings;
pub(crate) use settings::remove_formatters_on_save;
}
pub(crate) mod m_2025_10_03 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_10_16 {
mod settings;
pub(crate) use settings::restore_code_actions_on_format;
}
pub(crate) mod m_2025_10_17 {
mod settings;
pub(crate) use settings::make_file_finder_include_ignored_an_enum;
}
pub(crate) mod m_2025_10_21 {
mod settings;
pub(crate) use settings::make_relative_line_numbers_an_enum;
}
pub(crate) mod m_2025_11_12 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_11_20 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_11_25 {
mod settings;
pub(crate) use settings::remove_context_server_source;
}
pub(crate) mod m_2025_12_01 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_12_08 {
mod keymap;
pub(crate) use keymap::KEYMAP_PATTERNS;
}
pub(crate) mod m_2025_12_15 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2025_01_27 {
mod settings;
pub(crate) use settings::make_auto_indent_an_enum;
}
pub(crate) mod m_2026_02_02 {
mod settings;
pub(crate) use settings::move_edit_prediction_provider_to_edit_predictions;
}
pub(crate) mod m_2026_02_03 {
mod settings;
pub(crate) use settings::migrate_experimental_sweep_mercury;
}
pub(crate) mod m_2026_02_04 {
mod settings;
pub(crate) use settings::migrate_tool_permission_defaults;
}
pub(crate) mod m_2026_02_25 {
mod settings;
pub(crate) use settings::migrate_builtin_agent_servers_to_registry;
}
pub(crate) mod m_2026_03_16 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}
pub(crate) mod m_2026_03_23 {
mod keymap;
pub(crate) use keymap::KEYMAP_PATTERNS;
}
pub(crate) mod m_2026_03_30 {
mod settings;
pub(crate) use settings::make_play_sound_when_agent_done_an_enum;
}
pub(crate) mod m_2026_04_01 {
mod settings;
pub(crate) use settings::restructure_profiles_with_settings_key;
}
pub(crate) mod m_2026_04_10 {
mod settings;
pub(crate) use settings::rename_web_search_to_search_web;
}
pub(crate) mod m_2026_04_17 {
mod settings;
pub(crate) use settings::promote_show_branch_icon_true_to_show_branch_status_icon;
}
pub(crate) mod m_2026_05_04 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
}

View File

@@ -0,0 +1,62 @@
use collections::HashMap;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
replace_deprecated_settings_values,
)];
fn replace_deprecated_settings_values(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
let parent_object_range = mat
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
let parent_object_name = contents.get(parent_object_range)?;
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_name_range)?;
let setting_value_ix = query.capture_index_for_name("setting_value")?;
let setting_value_range = mat
.nodes_for_capture_index(setting_value_ix)
.next()?
.byte_range();
let setting_value = contents.get(setting_value_range.clone())?;
UPDATED_SETTINGS
.get(&(parent_object_name, setting_name))
.and_then(|new_values| {
new_values
.iter()
.find_map(|(old_value, new_value)| {
(*old_value == setting_value).then(|| new_value.to_string())
})
.map(|new_value| (setting_value_range, new_value))
})
}
static UPDATED_SETTINGS: LazyLock<HashMap<(&str, &str), Vec<(&str, &str)>>> = LazyLock::new(|| {
HashMap::from_iter([
(
("chat_panel", "button"),
vec![("true", "\"always\""), ("false", "\"never\"")],
),
(
("scrollbar", "diagnostics"),
vec![("true", "\"all\""), ("false", "\"none\"")],
),
])
});

View File

@@ -0,0 +1,27 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_language_setting;
pub fn make_auto_indent_an_enum(value: &mut Value) -> Result<()> {
migrate_language_setting(value, migrate_auto_indent)
}
fn migrate_auto_indent(value: &mut Value, _path: &[&str]) -> Result<()> {
let Some(auto_indent) = value
.as_object_mut()
.and_then(|obj| obj.get_mut("auto_indent"))
else {
return Ok(());
};
*auto_indent = match auto_indent {
Value::Bool(true) => Value::String("syntax_aware".to_string()),
Value::Bool(false) => Value::String("none".to_string()),
Value::String(s) if s == "syntax_aware" || s == "preserve_indent" || s == "none" => {
return Ok(());
}
_ => anyhow::bail!("Expected auto_indent to be a boolean or valid enum value"),
};
Ok(())
}

View File

@@ -0,0 +1,307 @@
use collections::HashMap;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::{
KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, KEYMAP_ACTION_ARRAY_PATTERN,
KEYMAP_ACTION_STRING_PATTERN, KEYMAP_CONTEXT_PATTERN,
};
pub const KEYMAP_PATTERNS: MigrationPatterns = &[
(
KEYMAP_ACTION_ARRAY_PATTERN,
replace_array_with_single_string,
),
(
KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN,
replace_action_argument_object_with_single_value,
),
(KEYMAP_ACTION_STRING_PATTERN, replace_string_action),
(KEYMAP_CONTEXT_PATTERN, rename_context_key),
];
fn replace_array_with_single_string(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let array_ix = query.capture_index_for_name("array")?;
let action_name_ix = query.capture_index_for_name("action_name")?;
let argument_ix = query.capture_index_for_name("argument")?;
let action_name = contents.get(
mat.nodes_for_capture_index(action_name_ix)
.next()?
.byte_range(),
)?;
let argument = contents.get(
mat.nodes_for_capture_index(argument_ix)
.next()?
.byte_range(),
)?;
let replacement = TRANSFORM_ARRAY.get(&(action_name, argument))?;
let replacement_as_string = format!("\"{replacement}\"");
let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
Some((range_to_replace, replacement_as_string))
}
static TRANSFORM_ARRAY: LazyLock<HashMap<(&str, &str), &str>> = LazyLock::new(|| {
HashMap::from_iter([
// activate
(
("workspace::ActivatePaneInDirection", "Up"),
"workspace::ActivatePaneUp",
),
(
("workspace::ActivatePaneInDirection", "Down"),
"workspace::ActivatePaneDown",
),
(
("workspace::ActivatePaneInDirection", "Left"),
"workspace::ActivatePaneLeft",
),
(
("workspace::ActivatePaneInDirection", "Right"),
"workspace::ActivatePaneRight",
),
// swap
(
("workspace::SwapPaneInDirection", "Up"),
"workspace::SwapPaneUp",
),
(
("workspace::SwapPaneInDirection", "Down"),
"workspace::SwapPaneDown",
),
(
("workspace::SwapPaneInDirection", "Left"),
"workspace::SwapPaneLeft",
),
(
("workspace::SwapPaneInDirection", "Right"),
"workspace::SwapPaneRight",
),
// menu
(
("app_menu::NavigateApplicationMenuInDirection", "Left"),
"app_menu::ActivateMenuLeft",
),
(
("app_menu::NavigateApplicationMenuInDirection", "Right"),
"app_menu::ActivateMenuRight",
),
// vim push
(("vim::PushOperator", "Change"), "vim::PushChange"),
(("vim::PushOperator", "Delete"), "vim::PushDelete"),
(("vim::PushOperator", "Yank"), "vim::PushYank"),
(("vim::PushOperator", "Replace"), "vim::PushReplace"),
(
("vim::PushOperator", "DeleteSurrounds"),
"vim::PushDeleteSurrounds",
),
(("vim::PushOperator", "Mark"), "vim::PushMark"),
(("vim::PushOperator", "Indent"), "vim::PushIndent"),
(("vim::PushOperator", "Outdent"), "vim::PushOutdent"),
(("vim::PushOperator", "AutoIndent"), "vim::PushAutoIndent"),
(("vim::PushOperator", "Rewrap"), "vim::PushRewrap"),
(
("vim::PushOperator", "ShellCommand"),
"vim::PushShellCommand",
),
(("vim::PushOperator", "Lowercase"), "vim::PushLowercase"),
(("vim::PushOperator", "Uppercase"), "vim::PushUppercase"),
(
("vim::PushOperator", "OppositeCase"),
"vim::PushOppositeCase",
),
(("vim::PushOperator", "Register"), "vim::PushRegister"),
(
("vim::PushOperator", "RecordRegister"),
"vim::PushRecordRegister",
),
(
("vim::PushOperator", "ReplayRegister"),
"vim::PushReplayRegister",
),
(
("vim::PushOperator", "ReplaceWithRegister"),
"vim::PushReplaceWithRegister",
),
(
("vim::PushOperator", "ToggleComments"),
"vim::PushToggleComments",
),
// vim switch
(("vim::SwitchMode", "Normal"), "vim::SwitchToNormalMode"),
(("vim::SwitchMode", "Insert"), "vim::SwitchToInsertMode"),
(("vim::SwitchMode", "Replace"), "vim::SwitchToReplaceMode"),
(("vim::SwitchMode", "Visual"), "vim::SwitchToVisualMode"),
(
("vim::SwitchMode", "VisualLine"),
"vim::SwitchToVisualLineMode",
),
(
("vim::SwitchMode", "VisualBlock"),
"vim::SwitchToVisualBlockMode",
),
(
("vim::SwitchMode", "HelixNormal"),
"vim::SwitchToHelixNormalMode",
),
// vim resize
(("vim::ResizePane", "Widen"), "vim::ResizePaneRight"),
(("vim::ResizePane", "Narrow"), "vim::ResizePaneLeft"),
(("vim::ResizePane", "Shorten"), "vim::ResizePaneDown"),
(("vim::ResizePane", "Lengthen"), "vim::ResizePaneUp"),
// fold at level
(("editor::FoldAtLevel", "1"), "editor::FoldAtLevel1"),
(("editor::FoldAtLevel", "2"), "editor::FoldAtLevel2"),
(("editor::FoldAtLevel", "3"), "editor::FoldAtLevel3"),
(("editor::FoldAtLevel", "4"), "editor::FoldAtLevel4"),
(("editor::FoldAtLevel", "5"), "editor::FoldAtLevel5"),
(("editor::FoldAtLevel", "6"), "editor::FoldAtLevel6"),
(("editor::FoldAtLevel", "7"), "editor::FoldAtLevel7"),
(("editor::FoldAtLevel", "8"), "editor::FoldAtLevel8"),
(("editor::FoldAtLevel", "9"), "editor::FoldAtLevel9"),
])
});
/// [ "editor::FoldAtLevel", { "level": 1 } ] -> [ "editor::FoldAtLevel", 1 ]
fn replace_action_argument_object_with_single_value(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let array_ix = query.capture_index_for_name("array")?;
let action_name_ix = query.capture_index_for_name("action_name")?;
let argument_key_ix = query.capture_index_for_name("argument_key")?;
let argument_value_ix = query.capture_index_for_name("argument_value")?;
let action_name = contents.get(
mat.nodes_for_capture_index(action_name_ix)
.next()?
.byte_range(),
)?;
let argument_key = contents.get(
mat.nodes_for_capture_index(argument_key_ix)
.next()?
.byte_range(),
)?;
let argument_value = contents.get(
mat.nodes_for_capture_index(argument_value_ix)
.next()?
.byte_range(),
)?;
let new_action_name = UNWRAP_OBJECTS.get(&action_name)?.get(&argument_key)?;
let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
let replacement = format!("[\"{}\", {}]", new_action_name, argument_value);
Some((range_to_replace, replacement))
}
/// "ctrl-k ctrl-1": [ "editor::PushOperator", { "Object": {} } ] -> [ "editor::vim::PushObject", {} ]
static UNWRAP_OBJECTS: LazyLock<HashMap<&str, HashMap<&str, &str>>> = LazyLock::new(|| {
HashMap::from_iter([
(
"editor::FoldAtLevel",
HashMap::from_iter([("level", "editor::FoldAtLevel")]),
),
(
"vim::PushOperator",
HashMap::from_iter([
("Object", "vim::PushObject"),
("FindForward", "vim::PushFindForward"),
("FindBackward", "vim::PushFindBackward"),
("Sneak", "vim::PushSneak"),
("SneakBackward", "vim::PushSneakBackward"),
("AddSurrounds", "vim::PushAddSurrounds"),
("ChangeSurrounds", "vim::PushChangeSurrounds"),
("Jump", "vim::PushJump"),
("Digraph", "vim::PushDigraph"),
("Literal", "vim::PushLiteral"),
]),
),
])
});
fn replace_string_action(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let action_name_ix = query.capture_index_for_name("action_name")?;
let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?;
let action_name_range = action_name_node.byte_range();
let action_name = contents.get(action_name_range.clone())?;
if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
return Some((action_name_range, new_action_name.to_string()));
}
None
}
/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu"
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from_iter([
(
"inline_completion::ToggleMenu",
"edit_prediction::ToggleMenu",
),
("editor::NextInlineCompletion", "editor::NextEditPrediction"),
(
"editor::PreviousInlineCompletion",
"editor::PreviousEditPrediction",
),
(
"editor::AcceptPartialInlineCompletion",
"editor::AcceptPartialEditPrediction",
),
("editor::ShowInlineCompletion", "editor::ShowEditPrediction"),
(
"editor::AcceptInlineCompletion",
"editor::AcceptEditPrediction",
),
(
"editor::ToggleInlineCompletions",
"editor::ToggleEditPrediction",
),
])
});
fn rename_context_key(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
let context_predicate_range = mat
.nodes_for_capture_index(context_predicate_ix)
.next()?
.byte_range();
let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
let mut new_predicate = old_predicate.to_string();
for (old_key, new_key) in CONTEXT_REPLACE.iter() {
new_predicate = new_predicate.replace(old_key, new_key);
}
if new_predicate != old_predicate {
Some((context_predicate_range, new_predicate))
} else {
None
}
}
/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions"
pub static CONTEXT_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from_iter([
("inline_completion", "edit_prediction"),
(
"inline_completion_requires_modifier",
"edit_prediction_requires_modifier",
),
])
});

View File

@@ -0,0 +1,101 @@
use collections::HashMap;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::{
SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
};
pub const SETTINGS_PATTERNS: MigrationPatterns = &[
(SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_name),
(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
replace_edit_prediction_provider_setting,
),
(SETTINGS_LANGUAGES_PATTERN, replace_setting_in_languages),
];
fn replace_setting_name(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let setting_capture_ix = query.capture_index_for_name("name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_capture_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_name_range.clone())?;
let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?;
Some((setting_name_range, new_setting_name.to_string()))
}
pub static SETTINGS_STRING_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(|| {
HashMap::from_iter([
(
"show_inline_completions_in_menu",
"show_edit_predictions_in_menu",
),
("show_inline_completions", "show_edit_predictions"),
(
"inline_completions_disabled_in",
"edit_predictions_disabled_in",
),
("inline_completions", "edit_predictions"),
])
});
pub fn replace_edit_prediction_provider_setting(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
let parent_object_range = mat
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
let parent_object_name = contents.get(parent_object_range)?;
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_range.clone())?;
if parent_object_name == "features" && setting_name == "inline_completion_provider" {
return Some((setting_range, "edit_prediction_provider".into()));
}
None
}
fn replace_setting_in_languages(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let setting_capture_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_capture_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_name_range.clone())?;
let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?;
Some((setting_name_range, new_setting_name.to_string()))
}
static LANGUAGE_SETTINGS_REPLACE: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(|| {
HashMap::from_iter([
("show_inline_completions", "show_edit_predictions"),
(
"inline_completions_disabled_in",
"edit_predictions_disabled_in",
),
])
});

View File

@@ -0,0 +1,82 @@
use collections::HashMap;
use convert_case::{Case, Casing};
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN;
pub const KEYMAP_PATTERNS: MigrationPatterns = &[(
KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN,
action_argument_snake_case,
)];
fn to_snake_case(text: &str) -> String {
text.to_case(Case::Snake)
}
fn action_argument_snake_case(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let array_ix = query.capture_index_for_name("array")?;
let action_name_ix = query.capture_index_for_name("action_name")?;
let argument_key_ix = query.capture_index_for_name("argument_key")?;
let argument_value_ix = query.capture_index_for_name("argument_value")?;
let action_name = contents.get(
mat.nodes_for_capture_index(action_name_ix)
.next()?
.byte_range(),
)?;
let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?;
let argument_key = contents.get(
mat.nodes_for_capture_index(argument_key_ix)
.next()?
.byte_range(),
)?;
if argument_key != *replacement_key {
return None;
}
let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?;
let argument_value = contents.get(argument_value_node.byte_range())?;
let new_key = to_snake_case(argument_key);
let new_value = if argument_value_node.kind() == "string" {
format!("\"{}\"", to_snake_case(argument_value.trim_matches('"')))
} else {
argument_value.to_string()
};
let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
let replacement = format!(
"[\"{}\", {{ \"{}\": {} }}]",
action_name, new_key, new_value
);
Some((range_to_replace, replacement))
}
static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from_iter([
("vim::NextWordStart", "ignorePunctuation"),
("vim::NextWordEnd", "ignorePunctuation"),
("vim::PreviousWordStart", "ignorePunctuation"),
("vim::PreviousWordEnd", "ignorePunctuation"),
("vim::MoveToNext", "partialWord"),
("vim::MoveToPrev", "partialWord"),
("vim::Down", "displayLines"),
("vim::Up", "displayLines"),
("vim::EndOfLine", "displayLines"),
("vim::StartOfLine", "displayLines"),
("vim::FirstNonWhitespace", "displayLines"),
("pane::CloseActiveItem", "saveIntent"),
("vim::Paste", "preserveClipboard"),
("vim::Word", "ignorePunctuation"),
("vim::Subword", "ignorePunctuation"),
("vim::IndentObj", "includeBelow"),
])
});

View File

@@ -0,0 +1,83 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[
(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
replace_tab_close_button_setting_key,
),
(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
replace_tab_close_button_setting_value,
),
];
fn replace_tab_close_button_setting_key(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
let parent_object_range = mat
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
let parent_object_name = contents.get(parent_object_range)?;
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_range.clone())?;
if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
return Some((setting_range, "show_close_button".into()));
}
None
}
fn replace_tab_close_button_setting_value(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let parent_object_capture_ix = query.capture_index_for_name("parent_key")?;
let parent_object_range = mat
.nodes_for_capture_index(parent_object_capture_ix)
.next()?
.byte_range();
let parent_object_name = contents.get(parent_object_range)?;
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_name_range)?;
let setting_value_ix = query.capture_index_for_name("setting_value")?;
let setting_value_range = mat
.nodes_for_capture_index(setting_value_ix)
.next()?
.byte_range();
let setting_value = contents.get(setting_value_range.clone())?;
if parent_object_name == "tabs" && setting_name == "always_show_close_button" {
match setting_value {
"true" => {
return Some((setting_value_range, "\"always\"".to_string()));
}
"false" => {
return Some((setting_value_range, "\"hover\"".to_string()));
}
_ => {}
}
}
None
}

View File

@@ -0,0 +1,75 @@
use collections::HashMap;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::KEYMAP_ACTION_STRING_PATTERN;
pub const KEYMAP_PATTERNS: MigrationPatterns =
&[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)];
fn replace_string_action(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let action_name_ix = query.capture_index_for_name("action_name")?;
let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?;
let action_name_range = action_name_node.byte_range();
let action_name = contents.get(action_name_range.clone())?;
if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
return Some((action_name_range, new_action_name.to_string()));
}
if let Some((new_action_name, options)) = STRING_TO_ARRAY_REPLACE.get(action_name) {
let full_string_range = action_name_node.parent()?.byte_range();
let mut options_parts = Vec::new();
for (key, value) in options.iter() {
options_parts.push(format!("\"{}\": {}", key, value));
}
let options_str = options_parts.join(", ");
let replacement = format!("[\"{}\", {{ {} }}]", new_action_name, options_str);
return Some((full_string_range, replacement));
}
None
}
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from_iter([
(
"editor::GoToPrevDiagnostic",
"editor::GoToPreviousDiagnostic",
),
("editor::ContextMenuPrev", "editor::ContextMenuPrevious"),
("search::SelectPrevMatch", "search::SelectPreviousMatch"),
("file_finder::SelectPrev", "file_finder::SelectPrevious"),
("menu::SelectPrev", "menu::SelectPrevious"),
("editor::TabPrev", "editor::Backtab"),
("pane::ActivatePrevItem", "pane::ActivatePreviousItem"),
("vim::MoveToPrev", "vim::MoveToPrevious"),
("vim::MoveToPrevMatch", "vim::MoveToPreviousMatch"),
])
});
/// "editor::GoToPrevHunk" -> ["editor::GoToPreviousHunk", { "center_cursor": true }]
static STRING_TO_ARRAY_REPLACE: LazyLock<HashMap<&str, (&str, HashMap<&str, bool>)>> =
LazyLock::new(|| {
HashMap::from_iter([
(
"editor::GoToHunk",
(
"editor::GoToHunk",
HashMap::from_iter([("center_cursor", true)]),
),
),
(
"editor::GoToPrevHunk",
(
"editor::GoToPreviousHunk",
HashMap::from_iter([("center_cursor", true)]),
),
),
])
});

View File

@@ -0,0 +1,38 @@
use collections::HashSet;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN;
pub const KEYMAP_PATTERNS: MigrationPatterns = &[(
KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN,
replace_array_with_single_string,
)];
fn replace_array_with_single_string(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let array_ix = query.capture_index_for_name("array")?;
let action_name_ix = query.capture_index_for_name("action_name")?;
let action_name = contents.get(
mat.nodes_for_capture_index(action_name_ix)
.next()?
.byte_range(),
)?;
if TRANSFORM_ARRAY.contains(&action_name) {
let replacement_as_string = format!("\"{action_name}\"");
let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range();
return Some((range_to_replace, replacement_as_string));
}
None
}
/// ["editor::GoToPreviousHunk", { "center_cursor": true }] -> "editor::GoToPreviousHunk"
static TRANSFORM_ARRAY: LazyLock<HashSet<&str>> =
LazyLock::new(|| HashSet::from_iter(["editor::GoToHunk", "editor::GoToPreviousHunk"]));

View File

@@ -0,0 +1,65 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[
(SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_name),
(SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_value),
];
fn replace_setting_value(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let setting_capture_ix = query.capture_index_for_name("name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_capture_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_name_range)?;
if setting_name != "hide_mouse_while_typing" {
return None;
}
let value_capture_ix = query.capture_index_for_name("value")?;
let value_range = mat
.nodes_for_capture_index(value_capture_ix)
.next()?
.byte_range();
let value = contents.get(value_range.clone())?;
let new_value = if value.trim() == "true" {
"\"on_typing_and_movement\""
} else if value.trim() == "false" {
"\"never\""
} else {
return None;
};
Some((value_range, new_value.to_string()))
}
fn replace_setting_name(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let setting_capture_ix = query.capture_index_for_name("name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_capture_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_name_range.clone())?;
let new_setting_name = if setting_name == "hide_mouse_while_typing" {
"hide_mouse"
} else {
return None;
};
Some((setting_name_range, new_setting_name.to_string()))
}

View File

@@ -0,0 +1,31 @@
use collections::HashMap;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::KEYMAP_ACTION_STRING_PATTERN;
pub const KEYMAP_PATTERNS: MigrationPatterns =
&[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)];
fn replace_string_action(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let action_name_ix = query.capture_index_for_name("action_name")?;
let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?;
let action_name_range = action_name_node.byte_range();
let action_name = contents.get(action_name_range.clone())?;
if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
return Some((action_name_range, new_action_name.to_string()));
}
None
}
/// "space": "outline_panel::Open" -> "outline_panel::OpenSelectedEntry"
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from_iter([("outline_panel::Open", "outline_panel::OpenSelectedEntry")])
});

View File

@@ -0,0 +1,29 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_ASSISTANT_TOOLS_PATTERN,
replace_bash_with_terminal_in_profiles,
)];
fn replace_bash_with_terminal_in_profiles(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let tool_name_capture_ix = query.capture_index_for_name("tool_name")?;
let tool_name_range = mat
.nodes_for_capture_index(tool_name_capture_ix)
.next()?
.byte_range();
let tool_name = contents.get(tool_name_range.clone())?;
if tool_name != "bash" {
return None;
}
Some((tool_name_range, "terminal".to_string()))
}

View File

@@ -0,0 +1,25 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns =
&[(SETTINGS_ASSISTANT_TOOLS_PATTERN, rename_tools)];
fn rename_tools(contents: &str, mat: &QueryMatch, query: &Query) -> Option<(Range<usize>, String)> {
let tool_name_capture_ix = query.capture_index_for_name("tool_name")?;
let tool_name_range = mat
.nodes_for_capture_index(tool_name_capture_ix)
.next()?
.byte_range();
let tool_name = contents.get(tool_name_range.clone())?;
let new_name = match tool_name {
"find_replace_file" => "edit_file",
"regex_search" => "grep",
_ => return None,
};
Some((tool_name_range, new_name.to_string()))
}

View File

@@ -0,0 +1,27 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns =
&[(SETTINGS_ASSISTANT_TOOLS_PATTERN, rename_path_search_tool)];
fn rename_path_search_tool(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let tool_name_capture_ix = query.capture_index_for_name("tool_name")?;
let tool_name_range = mat
.nodes_for_capture_index(tool_name_capture_ix)
.next()?
.byte_range();
let tool_name = contents.get(tool_name_range.clone())?;
if tool_name == "path_search" {
return Some((tool_name_range, "find_path".to_string()));
}
None
}

View File

@@ -0,0 +1,41 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::{
MigrationPatterns, patterns::SETTINGS_ASSISTANT_PATTERN,
patterns::SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
};
pub const SETTINGS_PATTERNS: MigrationPatterns = &[
(SETTINGS_ASSISTANT_PATTERN, rename_assistant),
(
SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
rename_edit_prediction_assistant,
),
];
fn rename_assistant(
_contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let key_capture_ix = query.capture_index_for_name("key")?;
let key_range = mat
.nodes_for_capture_index(key_capture_ix)
.next()?
.byte_range();
Some((key_range, "agent".to_string()))
}
fn rename_edit_prediction_assistant(
_contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let key_capture_ix = query.capture_index_for_name("enabled_in_assistant")?;
let key_range = mat
.nodes_for_capture_index(key_capture_ix)
.next()?
.byte_range();
Some((key_range, "enabled_in_text_threads".to_string()))
}

View File

@@ -0,0 +1,26 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::{MigrationPatterns, patterns::SETTINGS_DUPLICATED_AGENT_PATTERN};
pub const SETTINGS_PATTERNS: MigrationPatterns =
&[(SETTINGS_DUPLICATED_AGENT_PATTERN, comment_duplicated_agent)];
fn comment_duplicated_agent(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let pair_ix = query.capture_index_for_name("pair1")?;
let mut range = mat.nodes_for_capture_index(pair_ix).next()?.byte_range();
// Include the comma into the commented region
let rtext = &contents[range.end..];
if let Some(comma_index) = rtext.find(',') {
range.end += comma_index + 1;
}
let value = contents[range.clone()].to_string();
let commented_value = format!("/* Duplicated key auto-commented: {value} */");
Some((range, commented_value))
}

View File

@@ -0,0 +1,92 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_CONTEXT_SERVER_PATTERN,
migrate_context_server_settings,
)];
const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @context-servers)
value: (object
(pair
key: (string (string_content) @server-name)
value: (object) @server-settings
)
)
)
)
(#eq? @context-servers "context_servers")
)"#;
fn migrate_context_server_settings(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let server_settings_index = query.capture_index_for_name("server-settings")?;
let server_settings = mat.nodes_for_capture_index(server_settings_index).next()?;
let mut has_command = false;
let mut has_settings = false;
let mut has_url = false;
let mut other_keys = 0;
let mut column = None;
// Parse the server settings to check what keys it contains
let mut cursor = server_settings.walk();
for child in server_settings.children(&mut cursor) {
if child.kind() == "pair"
&& let Some(key_node) = child.child_by_field_name("key")
{
if let (None, Some(quote_content)) = (column, key_node.child(0)) {
column = Some(quote_content.start_position().column);
}
if let Some(string_content) = key_node.child(1) {
let key = &contents[string_content.byte_range()];
match key {
// If it already has a source key, don't modify it
"source" => return None,
"command" => has_command = true,
"settings" => has_settings = true,
"url" => has_url = true,
_ => other_keys += 1,
}
}
}
}
let source_type = if has_command { "custom" } else { "extension" };
// Insert the source key at the beginning of the object
let start = server_settings.start_byte() + 1;
let indent = " ".repeat(column.unwrap_or(12));
if !has_command && !has_settings && !has_url {
return Some((
start..start,
format!(
r#"
{indent}"source": "{}",
{indent}"settings": {{}}{}
"#,
source_type,
if other_keys > 0 { "," } else { "" }
),
));
}
Some((
start..start,
format!(
r#"
{indent}"source": "{}","#,
source_type
),
))
}

View File

@@ -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()
&& 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')
&& text_after[..newline_pos].chars().all(|c| c.is_whitespace())
{
range_to_remove.end += newline_pos + 1;
}
Some((range_to_remove, String::new()))
}

View File

@@ -0,0 +1,132 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_CONTEXT_SERVER_PATTERN,
flatten_context_server_command,
)];
const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @context-servers)
value: (object
(pair
key: (string (string_content) @server-name)
value: (object
(pair
key: (string (string_content) @source-key)
value: (string (string_content) @source-value)
)
(pair
key: (string (string_content) @command-key)
value: (object) @command-object
) @command-pair
) @server-settings
)
)
)
)
(#eq? @context-servers "context_servers")
(#eq? @source-key "source")
(#eq? @source-value "custom")
(#eq? @command-key "command")
)"#;
fn flatten_context_server_command(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let command_pair_index = query.capture_index_for_name("command-pair")?;
let command_pair = mat.nodes_for_capture_index(command_pair_index).next()?;
let command_object_index = query.capture_index_for_name("command-object")?;
let command_object = mat.nodes_for_capture_index(command_object_index).next()?;
let server_settings_index = query.capture_index_for_name("server-settings")?;
let _server_settings = mat.nodes_for_capture_index(server_settings_index).next()?;
// Parse the command object to extract path, args, and env
let mut path_value = None;
let mut args_value = None;
let mut env_value = None;
let mut cursor = command_object.walk();
for child in command_object.children(&mut cursor) {
if child.kind() == "pair"
&& let Some(key_node) = child.child_by_field_name("key")
&& let Some(string_content) = key_node.child(1)
{
let key = &contents[string_content.byte_range()];
if let Some(value_node) = child.child_by_field_name("value") {
let value_range = value_node.byte_range();
match key {
"path" => path_value = Some(&contents[value_range]),
"args" => args_value = Some(&contents[value_range]),
"env" => env_value = Some(&contents[value_range]),
_ => {}
}
}
}
}
let path = path_value?;
// Get the proper indentation from the command pair
let command_pair_start = command_pair.start_byte();
let line_start = contents[..command_pair_start]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or(0);
let indent = &contents[line_start..command_pair_start];
// Build the replacement string
let mut replacement = format!("\"command\": {}", path);
// Add args if present - need to reduce indentation
if let Some(args) = args_value {
replacement.push_str(",\n");
replacement.push_str(indent);
replacement.push_str("\"args\": ");
let reduced_args = reduce_indentation(args, 4);
replacement.push_str(&reduced_args);
}
// Add env if present - need to reduce indentation
if let Some(env) = env_value {
replacement.push_str(",\n");
replacement.push_str(indent);
replacement.push_str("\"env\": ");
replacement.push_str(&reduce_indentation(env, 4));
}
let range_to_replace = command_pair.byte_range();
Some((range_to_replace, replacement))
}
fn reduce_indentation(text: &str, spaces: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
let mut result = String::new();
for (i, line) in lines.iter().enumerate() {
if i > 0 {
result.push('\n');
}
// Count leading spaces
let leading_spaces = line.chars().take_while(|&c| c == ' ').count();
if leading_spaces >= spaces {
// Reduce indentation
result.push_str(&line[spaces..]);
} else {
// Keep line as is if it doesn't have enough indentation
result.push_str(line);
}
}
result
}

View File

@@ -0,0 +1,37 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_ROOT_KEY_VALUE_PATTERN,
migrate_drag_and_drop_selection,
)];
fn migrate_drag_and_drop_selection(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let name_ix = query.capture_index_for_name("name")?;
let name_range = mat.nodes_for_capture_index(name_ix).next()?.byte_range();
let name = contents.get(name_range)?;
if name != "drag_and_drop_selection" {
return None;
}
let value_ix = query.capture_index_for_name("value")?;
let value_node = mat.nodes_for_capture_index(value_ix).next()?;
let value_range = value_node.byte_range();
let value = contents.get(value_range.clone())?;
match value {
"true" | "false" => {
let replacement = format!("{{\n \"enabled\": {}\n }}", value);
Some((value_range, replacement))
}
_ => None,
}
}

View File

@@ -0,0 +1,74 @@
use crate::migrations::migrate_language_setting;
use anyhow::Result;
use serde_json::Value;
pub fn flatten_code_actions_formatters(value: &mut Value) -> Result<()> {
migrate_language_setting(value, |value, _path| {
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
for key in ["formatter", "format_on_save"] {
let Some(formatter) = obj.get_mut(key) else {
continue;
};
let new_formatter = match formatter {
Value::Array(arr) => {
let mut new_arr = Vec::new();
let mut found_code_actions = false;
for item in arr {
let Some(obj) = item.as_object() else {
new_arr.push(item.clone());
continue;
};
let code_actions_obj = obj
.get("code_actions")
.and_then(|code_actions| code_actions.as_object());
let Some(code_actions) = code_actions_obj else {
new_arr.push(item.clone());
continue;
};
found_code_actions = true;
for (name, enabled) in code_actions {
if !enabled.as_bool().unwrap_or(true) {
continue;
}
new_arr.push(serde_json::json!({
"code_action": name
}));
}
}
if !found_code_actions {
continue;
}
Value::Array(new_arr)
}
Value::Object(obj) => {
let mut new_arr = Vec::new();
let code_actions_obj = obj
.get("code_actions")
.and_then(|code_actions| code_actions.as_object());
let Some(code_actions) = code_actions_obj else {
continue;
};
for (name, enabled) in code_actions {
if !enabled.as_bool().unwrap_or(true) {
continue;
}
new_arr.push(serde_json::json!({
"code_action": name
}));
}
if new_arr.len() == 1 {
new_arr.pop().unwrap()
} else {
Value::Array(new_arr)
}
}
_ => continue,
};
obj.insert(key.to_string(), new_formatter);
}
return Ok(());
})
}

View File

@@ -0,0 +1,41 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_language_setting;
pub fn remove_formatters_on_save(value: &mut Value) -> Result<()> {
migrate_language_setting(value, remove_formatters_on_save_inner)
}
fn remove_formatters_on_save_inner(value: &mut Value, path: &[&str]) -> Result<()> {
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
let Some(format_on_save) = obj.get("format_on_save").cloned() else {
return Ok(());
};
let is_format_on_save_set_to_formatter = format_on_save
.as_str()
.map_or(true, |s| s != "on" && s != "off");
if !is_format_on_save_set_to_formatter {
return Ok(());
}
fn fmt_path(path: &[&str], key: &str) -> String {
let mut path = path.to_vec();
path.push(key);
path.join(".")
}
anyhow::ensure!(
obj.get("formatter").is_none(),
r#"Setting formatters in both "format_on_save" and "formatter" is deprecated. Please migrate the formatters from {} into {}"#,
fmt_path(path, "format_on_save"),
fmt_path(path, "formatter")
);
obj.insert("format_on_save".to_string(), serde_json::json!("on"));
obj.insert("formatter".to_string(), format_on_save);
Ok(())
}

View File

@@ -0,0 +1,30 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns =
&[(SETTINGS_ROOT_KEY_VALUE_PATTERN, rename_agent_font_size)];
/// Renames the setting `agent_font_size` to `agent_ui_font_size`
fn rename_agent_font_size(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let setting_capture_ix = query.capture_index_for_name("name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_capture_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_name_range.clone())?;
if setting_name != "agent_font_size" {
return None;
}
Some((setting_name_range, "agent_ui_font_size".to_string()))
}

View File

@@ -0,0 +1,71 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_language_setting;
pub fn restore_code_actions_on_format(value: &mut Value) -> Result<()> {
migrate_language_setting(value, restore_code_actions_on_format_inner)
}
fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> {
let Some(obj) = value.as_object_mut() else {
return Ok(());
};
let code_actions_on_format = obj
.get("code_actions_on_format")
.cloned()
.unwrap_or_else(|| Value::Object(Default::default()));
fn fmt_path(path: &[&str], key: &str) -> String {
let mut path = path.to_vec();
path.push(key);
path.join(".")
}
let Some(mut code_actions_map) = code_actions_on_format.as_object().cloned() else {
anyhow::bail!(
r#"The `code_actions_on_format` is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map<String, bool>"#,
fmt_path(path, "code_actions_on_format"),
);
};
let Some(formatter) = obj.get("formatter") else {
return Ok(());
};
let formatter_array = if let Some(array) = formatter.as_array() {
array.clone()
} else {
vec![formatter.clone()]
};
if formatter_array.is_empty() {
return Ok(());
}
let mut code_action_formatters = Vec::new();
for formatter in formatter_array {
let Some(code_action) = formatter.get("code_action") else {
return Ok(());
};
let Some(code_action_name) = code_action.as_str() else {
anyhow::bail!(
r#"The `code_action` is in an invalid state and cannot be migrated at {}. Please ensure the code_action setting is a String"#,
fmt_path(path, "formatter"),
);
};
code_action_formatters.push(code_action_name.to_string());
}
code_actions_map.extend(
code_action_formatters
.into_iter()
.rev()
.map(|code_action| (code_action, Value::Bool(true))),
);
obj.insert("formatter".to_string(), Value::Array(vec![]));
obj.insert(
"code_actions_on_format".into(),
Value::Object(code_actions_map),
);
Ok(())
}

View File

@@ -0,0 +1,30 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
pub fn make_file_finder_include_ignored_an_enum(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
let Some(file_finder) = obj.get_mut("file_finder") else {
return Ok(());
};
let Some(file_finder_obj) = file_finder.as_object_mut() else {
anyhow::bail!("Expected file_finder to be an object");
};
let Some(include_ignored) = file_finder_obj.get_mut("include_ignored") else {
return Ok(());
};
*include_ignored = match include_ignored {
Value::Bool(true) => Value::String("all".to_string()),
Value::Bool(false) => Value::String("indexed".to_string()),
Value::Null => Value::String("smart".to_string()),
Value::String(s) if s == "all" || s == "indexed" || s == "smart" => return Ok(()),
_ => anyhow::bail!("Expected include_ignored to be a boolean or null"),
};
Ok(())
}

View File

@@ -0,0 +1,22 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
pub fn make_relative_line_numbers_an_enum(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
let Some(relative_line_numbers) = obj.get_mut("relative_line_numbers") else {
return Ok(());
};
*relative_line_numbers = match relative_line_numbers {
Value::Bool(true) => Value::String("enabled".to_string()),
Value::Bool(false) => Value::String("disabled".to_string()),
Value::String(s) if s == "enabled" || s == "disabled" || s == "wrapped" => return Ok(()),
_ => anyhow::bail!("Expected relative_line_numbers to be a boolean"),
};
Ok(())
}

View File

@@ -0,0 +1,84 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[
(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
rename_open_file_on_paste_setting,
),
(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
replace_open_file_on_paste_setting_value,
),
];
fn rename_open_file_on_paste_setting(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
if !is_project_panel_open_file_on_paste(contents, mat, query) {
return None;
}
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
Some((setting_name_range, "auto_open".to_string()))
}
fn replace_open_file_on_paste_setting_value(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
if !is_project_panel_open_file_on_paste(contents, mat, query) {
return None;
}
let value_ix = query.capture_index_for_name("setting_value")?;
let value_node = mat.nodes_for_capture_index(value_ix).next()?;
let value_range = value_node.byte_range();
let value_text = contents.get(value_range.clone())?.trim();
let normalized_value = match value_text {
"true" => "true",
"false" => "false",
_ => return None,
};
Some((
value_range,
format!("{{ \"on_paste\": {normalized_value} }}"),
))
}
fn is_project_panel_open_file_on_paste(contents: &str, mat: &QueryMatch, query: &Query) -> bool {
let parent_key_ix = match query.capture_index_for_name("parent_key") {
Some(ix) => ix,
None => return false,
};
let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
if contents.get(parent_range) != Some("project_panel") {
return false;
}
let setting_name_ix = match query.capture_index_for_name("setting_name") {
Some(ix) => ix,
None => return false,
};
let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
contents.get(setting_name_range) == Some("open_file_on_paste")
}

View File

@@ -0,0 +1,76 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_AGENT_SERVERS_CUSTOM_PATTERN,
migrate_custom_agent_settings,
)];
const SETTINGS_AGENT_SERVERS_CUSTOM_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @agent-servers)
value: (object
(pair
key: (string (string_content) @server-name)
value: (object) @server-settings
)
)
)
)
(#eq? @agent-servers "agent_servers")
)"#;
fn migrate_custom_agent_settings(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let server_name_index = query.capture_index_for_name("server-name")?;
let server_name = mat.nodes_for_capture_index(server_name_index).next()?;
let server_name_text = &contents[server_name.byte_range()];
if matches!(server_name_text, "gemini" | "claude" | "codex") {
return None;
}
let server_settings_index = query.capture_index_for_name("server-settings")?;
let server_settings = mat.nodes_for_capture_index(server_settings_index).next()?;
let mut column = None;
// Parse the server settings to check what keys it contains
let mut cursor = server_settings.walk();
for child in server_settings.children(&mut cursor) {
if child.kind() == "pair" {
if let Some(key_node) = child.child_by_field_name("key") {
if let (None, Some(quote_content)) = (column, key_node.child(0)) {
column = Some(quote_content.start_position().column);
}
if let Some(string_content) = key_node.child(1) {
let key = &contents[string_content.byte_range()];
match key {
// If it already has a type key, don't modify it
"type" => return None,
_ => {}
}
}
}
}
}
// Insert the type key at the beginning of the object
let start = server_settings.start_byte() + 1;
let indent = " ".repeat(column.unwrap_or(12));
Some((
start..start,
format!(
r#"
{indent}"type": "custom","#
),
))
}

View File

@@ -0,0 +1,21 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
pub fn remove_context_server_source(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
if let Some(context_servers) = obj.get_mut("context_servers") {
if let Some(servers) = context_servers.as_object_mut() {
for (_, server) in servers.iter_mut() {
if let Some(server_obj) = server.as_object_mut() {
server_obj.remove("source");
}
}
}
}
Ok(())
}

View File

@@ -0,0 +1,55 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
rename_enable_preview_from_code_navigation_setting,
)];
fn rename_enable_preview_from_code_navigation_setting(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
if !is_enable_preview_from_code_navigation(contents, mat, query) {
return None;
}
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
Some((
setting_name_range,
"enable_keep_preview_on_code_navigation".to_string(),
))
}
fn is_enable_preview_from_code_navigation(contents: &str, mat: &QueryMatch, query: &Query) -> bool {
let parent_key_ix = match query.capture_index_for_name("parent_key") {
Some(ix) => ix,
None => return false,
};
let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
if contents.get(parent_range) != Some("preview_tabs") {
return false;
}
let setting_name_ix = match query.capture_index_for_name("setting_name") {
Some(ix) => ix,
None => return false,
};
let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
contents.get(setting_name_range) == Some("enable_preview_from_code_navigation")
}

View File

@@ -0,0 +1,33 @@
use collections::HashMap;
use std::{ops::Range, sync::LazyLock};
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::KEYMAP_ACTION_STRING_PATTERN;
pub const KEYMAP_PATTERNS: MigrationPatterns =
&[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)];
fn replace_string_action(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let action_name_ix = query.capture_index_for_name("action_name")?;
let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?;
let action_name_range = action_name_node.byte_range();
let action_name = contents.get(action_name_range.clone())?;
if let Some(new_action_name) = STRING_REPLACE.get(&action_name) {
return Some((action_name_range, new_action_name.to_string()));
}
None
}
static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
HashMap::from_iter([(
"editor::AcceptPartialEditPrediction",
"editor::AcceptNextWordEditPrediction",
)])
});

View File

@@ -0,0 +1,52 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
SETTINGS_NESTED_KEY_VALUE_PATTERN,
rename_restore_on_startup_values,
)];
fn rename_restore_on_startup_values(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
if !is_restore_on_startup_setting(contents, mat, query) {
return None;
}
let setting_value_ix = query.capture_index_for_name("setting_value")?;
let setting_value_range = mat
.nodes_for_capture_index(setting_value_ix)
.next()?
.byte_range();
let setting_value = contents.get(setting_value_range.clone())?;
// The value includes quotes, so we check for the quoted string
let new_value = match setting_value.trim() {
"\"none\"" => "\"empty_tab\"",
"\"welcome\"" => "\"launchpad\"",
_ => return None,
};
Some((setting_value_range, new_value.to_string()))
}
fn is_restore_on_startup_setting(contents: &str, mat: &QueryMatch, query: &Query) -> bool {
// Check that the parent key is "workspace" (since restore_on_startup is under workspace settings)
// Actually, restore_on_startup can be at the root level too, so we need to handle both cases
// The SETTINGS_NESTED_KEY_VALUE_PATTERN captures parent_key and setting_name
let setting_name_ix = match query.capture_index_for_name("setting_name") {
Some(ix) => ix,
None => return false,
};
let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
contents.get(setting_name_range) == Some("restore_on_startup")
}

View File

@@ -0,0 +1,42 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
pub fn move_edit_prediction_provider_to_edit_predictions(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
let Some(features) = obj.get_mut("features") else {
return Ok(());
};
let Some(features_obj) = features.as_object_mut() else {
return Ok(());
};
let Some(provider) = features_obj.remove("edit_prediction_provider") else {
return Ok(());
};
let features_is_empty = features_obj.is_empty();
if features_is_empty {
obj.remove("features");
}
let edit_predictions = obj
.entry("edit_predictions")
.or_insert_with(|| Value::Object(Default::default()));
let Some(edit_predictions_obj) = edit_predictions.as_object_mut() else {
return Ok(());
};
if !edit_predictions_obj.contains_key("provider") {
edit_predictions_obj.insert("provider".to_string(), provider);
}
Ok(())
}

View File

@@ -0,0 +1,54 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
pub fn migrate_experimental_sweep_mercury(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut |obj| {
migrate_one(obj);
Ok(())
})
}
fn migrate_one(obj: &mut serde_json::Map<String, Value>) {
if let Some(edit_predictions) = obj.get_mut("edit_predictions") {
if let Some(edit_predictions_obj) = edit_predictions.as_object_mut() {
migrate_provider_field(edit_predictions_obj, "provider");
}
}
if let Some(features) = obj.get_mut("features") {
if let Some(features_obj) = features.as_object_mut() {
migrate_provider_field(features_obj, "edit_prediction_provider");
}
}
}
fn migrate_provider_field(obj: &mut serde_json::Map<String, Value>, field_name: &str) {
let Some(provider) = obj.get(field_name) else {
return;
};
let Some(provider_obj) = provider.as_object() else {
return;
};
let Some(experimental_name) = provider_obj.get("experimental") else {
return;
};
let Some(name) = experimental_name.as_str() else {
return;
};
let provider_name = match name {
"sweep" | "mercury" => name,
"zeta2" => "zed",
_ => return,
};
obj.insert(
field_name.to_string(),
Value::String(provider_name.to_string()),
);
}

View File

@@ -0,0 +1,124 @@
use anyhow::{Result, bail};
use serde_json::Value;
use crate::migrations::migrate_settings;
const AGENT_KEY: &str = "agent";
const ALWAYS_ALLOW_TOOL_ACTIONS: &str = "always_allow_tool_actions";
const DEFAULT_KEY: &str = "default";
const DEFAULT_MODE_KEY: &str = "default_mode";
const PROFILES_KEY: &str = "profiles";
const TOOL_PERMISSIONS_KEY: &str = "tool_permissions";
const TOOLS_KEY: &str = "tools";
pub fn migrate_tool_permission_defaults(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
if let Some(agent) = obj.get_mut(AGENT_KEY) {
migrate_agent_with_profiles(agent)?;
}
Ok(())
}
fn migrate_agent_with_profiles(agent: &mut Value) -> Result<()> {
migrate_agent_tool_permissions(agent)?;
if let Some(agent_object) = agent.as_object_mut() {
if let Some(profiles) = agent_object.get_mut(PROFILES_KEY) {
if let Some(profiles_object) = profiles.as_object_mut() {
for (_profile_name, profile) in profiles_object.iter_mut() {
migrate_agent_tool_permissions(profile)?;
}
}
}
}
Ok(())
}
fn migrate_agent_tool_permissions(agent: &mut Value) -> Result<()> {
let Some(agent_object) = agent.as_object_mut() else {
return Ok(());
};
let should_migrate_always_allow = match agent_object.get(ALWAYS_ALLOW_TOOL_ACTIONS) {
Some(Value::Bool(true)) => {
agent_object.remove(ALWAYS_ALLOW_TOOL_ACTIONS);
true
}
Some(Value::Bool(false)) | Some(Value::Null) | None => {
agent_object.remove(ALWAYS_ALLOW_TOOL_ACTIONS);
false
}
Some(_) => {
// Non-boolean value — leave it in place so the schema validator
// can report it, rather than silently dropping user data.
false
}
};
if should_migrate_always_allow {
if matches!(
agent_object.get(TOOL_PERMISSIONS_KEY),
None | Some(Value::Null)
) {
agent_object.insert(
TOOL_PERMISSIONS_KEY.to_string(),
Value::Object(Default::default()),
);
}
let Some(Value::Object(tool_permissions_object)) =
agent_object.get_mut(TOOL_PERMISSIONS_KEY)
else {
bail!(
"agent.tool_permissions should be an object or null when migrating \
always_allow_tool_actions"
);
};
if !tool_permissions_object.contains_key(DEFAULT_KEY)
&& !tool_permissions_object.contains_key(DEFAULT_MODE_KEY)
{
tool_permissions_object
.insert(DEFAULT_KEY.to_string(), Value::String("allow".to_string()));
}
}
if let Some(tool_permissions) = agent_object.get_mut(TOOL_PERMISSIONS_KEY) {
migrate_default_mode_to_default(tool_permissions)?;
}
Ok(())
}
fn migrate_default_mode_to_default(tool_permissions: &mut Value) -> Result<()> {
let Some(tool_permissions_object) = tool_permissions.as_object_mut() else {
return Ok(());
};
if let Some(default_mode) = tool_permissions_object.remove(DEFAULT_MODE_KEY) {
if !tool_permissions_object.contains_key(DEFAULT_KEY) {
tool_permissions_object.insert(DEFAULT_KEY.to_string(), default_mode);
}
}
if let Some(tools) = tool_permissions_object.get_mut(TOOLS_KEY) {
if let Some(tools_object) = tools.as_object_mut() {
for (_tool_name, tool_rules) in tools_object.iter_mut() {
if let Some(tool_rules_object) = tool_rules.as_object_mut() {
if let Some(default_mode) = tool_rules_object.remove(DEFAULT_MODE_KEY) {
if !tool_rules_object.contains_key(DEFAULT_KEY) {
tool_rules_object.insert(DEFAULT_KEY.to_string(), default_mode);
}
}
}
}
}
}
Ok(())
}

View File

@@ -0,0 +1,161 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
const AGENT_SERVERS_KEY: &str = "agent_servers";
struct BuiltinMapping {
old_key: &'static str,
registry_key: &'static str,
}
const BUILTIN_MAPPINGS: &[BuiltinMapping] = &[
BuiltinMapping {
old_key: "gemini",
registry_key: "gemini",
},
BuiltinMapping {
old_key: "claude",
registry_key: "claude-acp",
},
BuiltinMapping {
old_key: "codex",
registry_key: "codex-acp",
},
];
const REGISTRY_COMPATIBLE_FIELDS: &[&str] = &[
"env",
"default_mode",
"default_model",
"favorite_models",
"default_config_options",
"favorite_config_option_values",
];
pub fn migrate_builtin_agent_servers_to_registry(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
let Some(agent_servers) = obj.get_mut(AGENT_SERVERS_KEY) else {
return Ok(());
};
let Some(servers_map) = agent_servers.as_object_mut() else {
return Ok(());
};
for mapping in BUILTIN_MAPPINGS {
migrate_builtin_entry(servers_map, mapping);
}
Ok(())
}
fn migrate_builtin_entry(
servers_map: &mut serde_json::Map<String, Value>,
mapping: &BuiltinMapping,
) {
// Check if the old key exists and needs migration before taking ownership.
let needs_migration = servers_map
.get(mapping.old_key)
.and_then(|v| v.as_object())
.is_some_and(|obj| !obj.contains_key("type"));
if !needs_migration {
return;
}
// When the registry key differs from the old key and the target already
// exists, just remove the stale old entry to avoid overwriting user data.
if mapping.old_key != mapping.registry_key && servers_map.contains_key(mapping.registry_key) {
servers_map.remove(mapping.old_key);
return;
}
let Some(old_entry) = servers_map.remove(mapping.old_key) else {
return;
};
let Some(old_obj) = old_entry.as_object() else {
return;
};
let has_command = old_obj.contains_key("command");
let ignore_system_version = old_obj
.get("ignore_system_version")
.and_then(|v| v.as_bool());
// A custom entry is needed when the user configured a custom binary
// or explicitly opted into using the system version via
// `ignore_system_version: false` (only meaningful for gemini).
let needs_custom = has_command
|| (mapping.old_key == "gemini" && matches!(ignore_system_version, Some(false)));
if needs_custom {
let local_key = format!("{}-custom", mapping.registry_key);
// Don't overwrite an existing `-custom` entry.
if servers_map.contains_key(&local_key) {
return;
}
let mut custom_obj = serde_json::Map::new();
custom_obj.insert("type".to_string(), Value::String("custom".to_string()));
if has_command {
if let Some(command) = old_obj.get("command") {
custom_obj.insert("command".to_string(), command.clone());
}
if let Some(args) = old_obj.get("args") {
if !args.as_array().is_some_and(|a| a.is_empty()) {
custom_obj.insert("args".to_string(), args.clone());
}
}
} else {
// ignore_system_version: false — the user wants the binary from $PATH
custom_obj.insert(
"command".to_string(),
Value::String(mapping.old_key.to_string()),
);
}
// Carry over all compatible fields to the custom entry.
for &field in REGISTRY_COMPATIBLE_FIELDS {
if let Some(value) = old_obj.get(field) {
match value {
Value::Array(arr) if arr.is_empty() => continue,
Value::Object(map) if map.is_empty() => continue,
Value::Null => continue,
_ => {
custom_obj.insert(field.to_string(), value.clone());
}
}
}
}
servers_map.insert(local_key, Value::Object(custom_obj));
} else {
// Build a registry entry with compatible fields only.
let mut registry_obj = serde_json::Map::new();
registry_obj.insert("type".to_string(), Value::String("registry".to_string()));
for &field in REGISTRY_COMPATIBLE_FIELDS {
if let Some(value) = old_obj.get(field) {
match value {
Value::Array(arr) if arr.is_empty() => continue,
Value::Object(map) if map.is_empty() => continue,
Value::Null => continue,
_ => {
registry_obj.insert(field.to_string(), value.clone());
}
}
}
}
servers_map.insert(
mapping.registry_key.to_string(),
Value::Object(registry_obj),
);
}
}

View File

@@ -0,0 +1,50 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns =
&[(SETTINGS_NESTED_KEY_VALUE_PATTERN, rename_heex_settings)];
fn rename_heex_settings(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
if !is_heex_settings(contents, mat, query) {
return None;
}
let setting_name_ix = query.capture_index_for_name("setting_name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_name_ix)
.next()?
.byte_range();
Some((setting_name_range, "HEEx".to_string()))
}
fn is_heex_settings(contents: &str, mat: &QueryMatch, query: &Query) -> bool {
let parent_key_ix = match query.capture_index_for_name("parent_key") {
Some(ix) => ix,
None => return false,
};
let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
if contents.get(parent_range) != Some("languages") {
return false;
}
let setting_name_ix = match query.capture_index_for_name("setting_name") {
Some(ix) => ix,
None => return false,
};
let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() {
Some(node) => node.byte_range(),
None => return false,
};
contents.get(setting_name_range) == Some("HEEX")
}

View File

@@ -0,0 +1,47 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
pub const KEYMAP_PATTERNS: MigrationPatterns =
&[(crate::patterns::KEYMAP_CONTEXT_PATTERN, rename_context_key)];
fn rename_context_key(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let context_predicate_ix = query.capture_index_for_name("context_predicate")?;
let context_predicate_range = mat
.nodes_for_capture_index(context_predicate_ix)
.next()?
.byte_range();
let old_predicate = contents.get(context_predicate_range.clone())?.to_string();
let mut new_predicate = old_predicate.clone();
const REPLACEMENTS: &[(&str, &str)] = &[
(
"edit_prediction_conflict && !showing_completions",
"(edit_prediction && in_leading_whitespace)",
),
(
"edit_prediction_conflict && showing_completions",
"(edit_prediction && showing_completions)",
),
(
"edit_prediction_conflict",
"(edit_prediction && (showing_completions || in_leading_whitespace))",
),
];
for (old, new) in REPLACEMENTS {
new_predicate = new_predicate.replace(old, new);
}
if new_predicate != old_predicate {
Some((context_predicate_range, new_predicate))
} else {
None
}
}

View File

@@ -0,0 +1,29 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
pub fn make_play_sound_when_agent_done_an_enum(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(obj: &mut serde_json::Map<String, Value>) -> Result<()> {
let Some(play_sound) = obj
.get_mut("agent")
.and_then(|agent| agent.as_object_mut())
.and_then(|agent| agent.get_mut("play_sound_when_agent_done"))
else {
return Ok(());
};
*play_sound = match play_sound {
Value::Bool(true) => Value::String("always".to_string()),
Value::Bool(false) => Value::String("never".to_string()),
Value::String(s) if s == "never" || s == "when_hidden" || s == "always" => return Ok(()),
_ => {
anyhow::bail!("Expected play_sound_when_agent_done to be a boolean or valid enum value")
}
};
Ok(())
}

View File

@@ -0,0 +1,29 @@
use anyhow::Result;
use serde_json::Value;
pub fn restructure_profiles_with_settings_key(value: &mut Value) -> Result<()> {
let Some(root_object) = value.as_object_mut() else {
return Ok(());
};
let Some(profiles) = root_object.get_mut("profiles") else {
return Ok(());
};
let Some(profiles_map) = profiles.as_object_mut() else {
return Ok(());
};
for profile_value in profiles_map.values_mut() {
if profile_value
.as_object()
.is_some_and(|m| m.contains_key("settings") || m.contains_key("base"))
{
continue;
}
*profile_value = serde_json::json!({ "settings": profile_value });
}
Ok(())
}

View File

@@ -0,0 +1,50 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
const AGENT_KEY: &str = "agent";
const PROFILES_KEY: &str = "profiles";
const TOOL_PERMISSIONS_KEY: &str = "tool_permissions";
const TOOLS_KEY: &str = "tools";
const OLD_TOOL_NAME: &str = "web_search";
const NEW_TOOL_NAME: &str = "search_web";
pub fn rename_web_search_to_search_web(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_agent_value)
}
fn migrate_agent_value(object: &mut serde_json::Map<String, Value>) -> Result<()> {
let Some(agent) = object.get_mut(AGENT_KEY).and_then(|v| v.as_object_mut()) else {
return Ok(());
};
if let Some(tools) = agent
.get_mut(TOOL_PERMISSIONS_KEY)
.and_then(|v| v.as_object_mut())
.and_then(|tp| tp.get_mut(TOOLS_KEY))
.and_then(|v| v.as_object_mut())
{
rename_key(tools);
}
if let Some(profiles) = agent.get_mut(PROFILES_KEY).and_then(|v| v.as_object_mut()) {
for (_profile_name, profile) in profiles.iter_mut() {
if let Some(tools) = profile
.as_object_mut()
.and_then(|p| p.get_mut(TOOLS_KEY))
.and_then(|v| v.as_object_mut())
{
rename_key(tools);
}
}
}
Ok(())
}
fn rename_key(tools: &mut serde_json::Map<String, Value>) {
if let Some(value) = tools.remove(OLD_TOOL_NAME) {
tools.insert(NEW_TOOL_NAME.to_string(), value);
}
}

View File

@@ -0,0 +1,47 @@
use anyhow::Result;
use serde_json::Value;
use crate::migrations::migrate_settings;
const SETTINGS_KEY: &str = "settings";
const TITLE_BAR_KEY: &str = "title_bar";
const OLD_KEY: &str = "show_branch_icon";
const NEW_KEY: &str = "show_branch_status_icon";
pub fn promote_show_branch_icon_true_to_show_branch_status_icon(value: &mut Value) -> Result<()> {
migrate_settings(value, &mut migrate_one)
}
fn migrate_one(object: &mut serde_json::Map<String, Value>) -> Result<()> {
migrate_title_bar_value(object);
if let Some(settings) = object
.get_mut(SETTINGS_KEY)
.and_then(|value| value.as_object_mut())
{
migrate_title_bar_value(settings);
}
Ok(())
}
fn migrate_title_bar_value(object: &mut serde_json::Map<String, Value>) {
let Some(title_bar) = object
.get_mut(TITLE_BAR_KEY)
.and_then(|value| value.as_object_mut())
else {
return;
};
let Some(old_value) = title_bar.remove(OLD_KEY) else {
return;
};
if title_bar.contains_key(NEW_KEY) {
return;
}
if old_value == Value::Bool(true) {
title_bar.insert(NEW_KEY.to_string(), Value::Bool(true));
}
}

View File

@@ -0,0 +1,38 @@
use std::ops::Range;
use tree_sitter::{Query, QueryMatch};
use crate::MigrationPatterns;
use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN;
pub const SETTINGS_PATTERNS: MigrationPatterns =
&[(SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_value)];
fn replace_setting_value(
contents: &str,
mat: &QueryMatch,
query: &Query,
) -> Option<(Range<usize>, String)> {
let setting_capture_ix = query.capture_index_for_name("name")?;
let setting_name_range = mat
.nodes_for_capture_index(setting_capture_ix)
.next()?
.byte_range();
let setting_name = contents.get(setting_name_range)?;
if setting_name != "hide_mouse" {
return None;
}
let value_capture_ix = query.capture_index_for_name("value")?;
let value_range = mat
.nodes_for_capture_index(value_capture_ix)
.next()?
.byte_range();
let value = contents.get(value_range.clone())?;
if value.trim() != "\"on_typing_and_movement\"" {
return None;
}
Some((value_range, "\"on_typing_and_action\"".to_string()))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
mod keymap;
mod settings;
pub(crate) use keymap::{
KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, KEYMAP_ACTION_ARRAY_PATTERN,
KEYMAP_ACTION_STRING_PATTERN, KEYMAP_CONTEXT_PATTERN,
};
pub(crate) use settings::{
SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN,
SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN,
SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN,
};

View File

@@ -0,0 +1,77 @@
pub const KEYMAP_ACTION_ARRAY_PATTERN: &str = r#"(document
(array
(object
(pair
key: (string (string_content) @name)
value: (
(object
(pair
key: (string)
value: ((array
. (string (string_content) @action_name)
. (string (string_content) @argument)
.)) @array
)
)
)
)
)
)
(#eq? @name "bindings")
)"#;
pub const KEYMAP_ACTION_STRING_PATTERN: &str = r#"(document
(array
(object
(pair
key: (string (string_content) @name)
value: (
(object
(pair
key: (string)
value: (string (string_content) @action_name)
)
)
)
)
)
)
(#eq? @name "bindings")
)"#;
pub const KEYMAP_CONTEXT_PATTERN: &str = r#"(document
(array
(object
(pair
key: (string (string_content) @name)
value: (string (string_content) @context_predicate)
)
)
)
(#eq? @name "context")
)"#;
pub const KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN: &str = r#"(document
(array
(object
(pair
key: (string (string_content) @name)
value: (
(object
(pair
key: (string)
value: ((array
. (string (string_content) @action_name)
. (object
(pair
key: (string (string_content) @argument_key)
value: (_) @argument_value))
. ) @array
))
)
)
)
)
)
(#eq? @name "bindings")
)"#;

View File

@@ -0,0 +1,110 @@
pub const SETTINGS_ROOT_KEY_VALUE_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @name)
value: (_) @value
)
)
)"#;
pub const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @parent_key)
value: (object
(pair
key: (string (string_content) @setting_name)
value: (_) @setting_value
)
)
)
)
)"#;
pub const SETTINGS_LANGUAGES_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @languages)
value: (object
(pair
key: (string)
value: (object
(pair
key: (string (string_content) @setting_name)
value: (_) @value
)
)
))
)
)
(#eq? @languages "languages")
)"#;
pub const SETTINGS_ASSISTANT_TOOLS_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @assistant)
value: (object
(pair
key: (string (string_content) @profiles)
value: (object
(pair
key: (_)
value: (object
(pair
key: (string (string_content) @tools_key)
value: (object
(pair
key: (string (string_content) @tool_name)
value: (_) @tool_value
)
)
)
)
)
)
)
)
)
)
(#eq? @assistant "assistant")
(#eq? @profiles "profiles")
(#eq? @tools_key "tools")
)"#;
pub const SETTINGS_ASSISTANT_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @key)
)
)
(#eq? @key "assistant")
)"#;
pub const SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @edit_predictions)
value: (object
(pair key: (string (string_content) @enabled_in_assistant))
)
)
)
(#eq? @edit_predictions "edit_predictions")
(#eq? @enabled_in_assistant "enabled_in_assistant")
)"#;
pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document
(object
(pair
key: (string (string_content) @agent1)
value: (_)
) @pair1
(pair
key: (string (string_content) @agent2)
value: (_)
)
)
(#eq? @agent1 "agent")
(#eq? @agent2 "agent")
)"#;