From 219d36f589987ea9cedc5443dbe51ba6876412cc Mon Sep 17 00:00:00 2001 From: smit <0xtimsb@gmail.com> Date: Thu, 6 Mar 2025 23:04:48 +0530 Subject: [PATCH] migrator: Add versioned migrations (#26215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is a drawback to how we currently write our migrations: For example: 1. Suppose we change one of our actions from a string to an array and rename it, then roll out the preview build: Before: `"ctrl-x": "editor::GoToPrevHunk"` Latest: `"ctrl-x": ["editor::GoToPreviousHunk", { "center_cursor": true }]` To handle this, we wrote migration `A` to convert the string to an array. 2. Now, suppose we decide to change it back to a string: - User who hasn't migrated yet on Preview: `"ctrl-x": "editor::GoToPrevHunk"` - User who has migrated on Preview: `"ctrl-x": ["editor::GoToPreviousHunk", { "center_cursor": true }]` - Latest: `"ctrl-x": "editor::GoToPreviousHunk"` To handle this, we would need to remove migration `A` and add two more migrations: - **Migration B**: `"ctrl-x": "editor::GoToPrevHunk"` -> `"ctrl-x": "editor::GoToPreviousHunk"` - **Migration C**: `"ctrl-x": ["editor::GoToPreviousHunk", { "center_cursor": true }]` -> `"ctrl-x": "editor::GoToPreviousHunk"` Nice. But over time, this keeps increasing, making it impossible to track outdated versions and handle all cases. Missing a case means users stuck on `"ctrl-x": "editor::GoToPrevHunk"` will remain there and won't be automatically migrated to the latest state. --- To fix this, we introduce versioned migrations. Instead of removing migration `A`, we simply write a new migration that takes the user to the latest version—i.e., in this case, migration `C`. - A user who hasn't migrated before will go through both migrations `A` and `C` in order. - A user who has already migrated will only go through `C`, since `A` wouldn't change anything for them. With incremental migrations, we only need to write migrations on top of the latest state (big win!), as know internally they all would be on latest state. You *must not* modify previous migrations. Always create new ones instead. This also serves as base for only prompting user to migrate, when feature reaches stable. That way, preview and stable keymap and settings are in sync. cc: @mgsloan @ConradIrwin @probably-neb Release Notes: - N/A --- crates/migrator/src/migrations.rs | 27 + .../src/migrations/m_2025_01_29/keymap.rs | 297 ++++++ .../src/migrations/m_2025_01_29/settings.rs | 101 ++ .../src/migrations/m_2025_01_30/keymap.rs | 82 ++ .../src/migrations/m_2025_01_30/settings.rs | 83 ++ .../src/migrations/m_2025_03_03/keymap.rs | 75 ++ .../src/migrations/m_2025_03_06/keymap.rs | 38 + crates/migrator/src/migrator.rs | 889 +++--------------- crates/migrator/src/patterns.rs | 11 + crates/migrator/src/patterns/keymap.rs | 77 ++ crates/migrator/src/patterns/settings.rs | 41 + 11 files changed, 948 insertions(+), 773 deletions(-) create mode 100644 crates/migrator/src/migrations.rs create mode 100644 crates/migrator/src/migrations/m_2025_01_29/keymap.rs create mode 100644 crates/migrator/src/migrations/m_2025_01_29/settings.rs create mode 100644 crates/migrator/src/migrations/m_2025_01_30/keymap.rs create mode 100644 crates/migrator/src/migrations/m_2025_01_30/settings.rs create mode 100644 crates/migrator/src/migrations/m_2025_03_03/keymap.rs create mode 100644 crates/migrator/src/migrations/m_2025_03_06/keymap.rs create mode 100644 crates/migrator/src/patterns.rs create mode 100644 crates/migrator/src/patterns/keymap.rs create mode 100644 crates/migrator/src/patterns/settings.rs diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs new file mode 100644 index 0000000000..2603919b8c --- /dev/null +++ b/crates/migrator/src/migrations.rs @@ -0,0 +1,27 @@ +pub(crate) mod m_2025_01_29 { + mod keymap; + mod settings; + + pub(crate) use keymap::KEYMAP_PATTERNS; + pub(crate) use settings::{replace_edit_prediction_provider_setting, SETTINGS_PATTERNS}; +} + +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; +} diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs new file mode 100644 index 0000000000..2be465470d --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs @@ -0,0 +1,297 @@ +use collections::HashMap; +use std::{ops::Range, sync::LazyLock}; +use tree_sitter::{Query, QueryMatch}; + +use crate::patterns::{ + KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, KEYMAP_ACTION_ARRAY_PATTERN, + KEYMAP_ACTION_STRING_PATTERN, KEYMAP_CONTEXT_PATTERN, +}; +use crate::MigrationPatterns; + +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, 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> = 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"), + ]) +}); + +/// [ "editor::FoldAtLevel", { "level": 1 } ] -> [ "editor::FoldAtLevel", 1 ] +fn replace_action_argument_object_with_single_value( + contents: &str, + mat: &QueryMatch, + query: &Query, +) -> Option<(Range, 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>> = 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, 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> = 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, 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.to_string())) + } else { + None + } +} + +/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions" +pub static CONTEXT_REPLACE: LazyLock> = LazyLock::new(|| { + HashMap::from_iter([ + ("inline_completion", "edit_prediction"), + ( + "inline_completion_requires_modifier", + "edit_prediction_requires_modifier", + ), + ]) +}); diff --git a/crates/migrator/src/migrations/m_2025_01_29/settings.rs b/crates/migrator/src/migrations/m_2025_01_29/settings.rs new file mode 100644 index 0000000000..ba11779c35 --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_01_29/settings.rs @@ -0,0 +1,101 @@ +use collections::HashMap; +use std::{ops::Range, sync::LazyLock}; +use tree_sitter::{Query, QueryMatch}; + +use crate::patterns::{ + SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN, +}; +use crate::MigrationPatterns; + +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, 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> = + 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, 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.clone())?; + + 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, 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> = + LazyLock::new(|| { + HashMap::from_iter([ + ("show_inline_completions", "show_edit_predictions"), + ( + "inline_completions_disabled_in", + "edit_predictions_disabled_in", + ), + ]) + }); diff --git a/crates/migrator/src/migrations/m_2025_01_30/keymap.rs b/crates/migrator/src/migrations/m_2025_01_30/keymap.rs new file mode 100644 index 0000000000..e660b99ab1 --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_01_30/keymap.rs @@ -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::patterns::KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN; +use crate::MigrationPatterns; + +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, 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> = 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"), + ]) +}); diff --git a/crates/migrator/src/migrations/m_2025_01_30/settings.rs b/crates/migrator/src/migrations/m_2025_01_30/settings.rs new file mode 100644 index 0000000000..bc65764151 --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_01_30/settings.rs @@ -0,0 +1,83 @@ +use std::ops::Range; +use tree_sitter::{Query, QueryMatch}; + +use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; +use crate::MigrationPatterns; + +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, 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.clone())?; + + 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, 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.clone())?; + + 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.clone())?; + + 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 +} diff --git a/crates/migrator/src/migrations/m_2025_03_03/keymap.rs b/crates/migrator/src/migrations/m_2025_03_03/keymap.rs new file mode 100644 index 0000000000..0ed7c9e88a --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_03_03/keymap.rs @@ -0,0 +1,75 @@ +use collections::HashMap; +use std::{ops::Range, sync::LazyLock}; +use tree_sitter::{Query, QueryMatch}; + +use crate::patterns::KEYMAP_ACTION_STRING_PATTERN; +use crate::MigrationPatterns; + +pub const KEYMAP_PATTERNS: MigrationPatterns = + &[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)]; + +fn replace_string_action( + contents: &str, + mat: &QueryMatch, + query: &Query, +) -> Option<(Range, 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> = 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)>> = + 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)]), + ), + ), + ]) + }); diff --git a/crates/migrator/src/migrations/m_2025_03_06/keymap.rs b/crates/migrator/src/migrations/m_2025_03_06/keymap.rs new file mode 100644 index 0000000000..6975cdafa2 --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_03_06/keymap.rs @@ -0,0 +1,38 @@ +use collections::HashSet; +use std::{ops::Range, sync::LazyLock}; +use tree_sitter::{Query, QueryMatch}; + +use crate::patterns::KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN; +use crate::MigrationPatterns; + +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, 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> = + LazyLock::new(|| HashSet::from_iter(["editor::GoToHunk", "editor::GoToPreviousHunk"])); diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index bc950c08a9..e1d5ef0199 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -1,10 +1,29 @@ +//! ## When to create a migration and why? +//! A migration is necessary when keymap actions or settings are renamed or transformed (e.g., from an array to a string, a string to an array, a boolean to an enum, etc.). +//! +//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally. +//! It also provides a quick way to migrate their existing settings to the latest state using button in UI. +//! +//! ## How to create a migration? +//! Migrations use Tree-sitter to query commonly used patterns, such as actions with a string or actions with an array where the second argument is an object, etc. +//! Once queried, *you can filter out the modified items* and write the replacement logic. +//! +//! You *must not* modify previous migrations; always create new ones instead. +//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state. +//! Modifying existing migrations means they will only work for users upgrading from version x-1 to x, but not from x-2 to x, and so on, where x is the latest version. +//! +//! You only need to write replacement logic for x-1 to x because you can be certain that, internally, every user will be at x-1, regardless of their on disk state. + use anyhow::{Context, Result}; -use collections::HashMap; -use convert_case::{Case, Casing}; use std::{cmp::Reverse, ops::Range, sync::LazyLock}; use streaming_iterator::StreamingIterator; use tree_sitter::{Query, QueryMatch}; +use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; + +mod migrations; +mod patterns; + fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result> { let mut parser = tree_sitter::Parser::new(); parser.set_language(&tree_sitter_json::LANGUAGE.into())?; @@ -46,26 +65,55 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result Result> { + let mut current_text = text.to_string(); + let mut result: Option = None; + for (patterns, query) in migrations.iter() { + if let Some(migrated_text) = migrate(¤t_text, patterns, query)? { + current_text = migrated_text.clone(); + result = Some(migrated_text); + } + } + Ok(result) +} + pub fn migrate_keymap(text: &str) -> Result> { - let transformed_text = migrate( - text, - KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS, - &KEYMAP_MIGRATION_TRANSFORMATION_QUERY, - )?; - let replacement_text = migrate( - &transformed_text.as_ref().unwrap_or(&text.to_string()), - KEYMAP_MIGRATION_REPLACEMENT_PATTERNS, - &KEYMAP_MIGRATION_REPLACEMENT_QUERY, - )?; - Ok(replacement_text.or(transformed_text)) + let migrations: &[(MigrationPatterns, &Query)] = &[ + ( + migrations::m_2025_01_29::KEYMAP_PATTERNS, + &KEYMAP_QUERY_2025_01_29, + ), + ( + migrations::m_2025_01_30::KEYMAP_PATTERNS, + &KEYMAP_QUERY_2025_01_30, + ), + ( + migrations::m_2025_03_03::KEYMAP_PATTERNS, + &KEYMAP_QUERY_2025_03_03, + ), + ( + migrations::m_2025_03_06::KEYMAP_PATTERNS, + &KEYMAP_QUERY_2025_03_06, + ), + ]; + run_migrations(text, migrations) } pub fn migrate_settings(text: &str) -> Result> { - migrate( - &text, - SETTINGS_MIGRATION_PATTERNS, - &SETTINGS_MIGRATION_QUERY, - ) + let migrations: &[(MigrationPatterns, &Query)] = &[ + ( + migrations::m_2025_01_29::SETTINGS_PATTERNS, + &SETTINGS_QUERY_2025_01_29, + ), + ( + migrations::m_2025_01_30::SETTINGS_PATTERNS, + &SETTINGS_QUERY_2025_01_30, + ), + ]; + run_migrations(text, migrations) } pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result> { @@ -73,578 +121,61 @@ pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result Option<(Range, String)>, )]; -const KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS: MigrationPatterns = &[ - (ACTION_ARRAY_PATTERN, replace_array_with_single_string), - ( - ACTION_ARGUMENT_OBJECT_PATTERN, - replace_action_argument_object_with_single_value, - ), - (ACTION_STRING_PATTERN, replace_string_action), - (CONTEXT_PREDICATE_PATTERN, rename_context_key), -]; - -static KEYMAP_MIGRATION_TRANSFORMATION_QUERY: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - &KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS - .iter() - .map(|pattern| pattern.0) - .collect::(), - ) - .unwrap() -}); - -const 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 - ) - ) - ) +macro_rules! define_query { + ($var_name:ident, $patterns_path:path) => { + static $var_name: LazyLock = LazyLock::new(|| { + Query::new( + &tree_sitter_json::LANGUAGE.into(), + &$patterns_path + .iter() + .map(|pattern| pattern.0) + .collect::(), ) - ) - ) - (#eq? @name "bindings") -)"#; - -fn replace_array_with_single_string( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, 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> = 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"), - ]) -}); - -const ACTION_ARGUMENT_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) @action_key) - value: (_) @argument)) - . ) @array - )) - ) - ) - ) - ) - ) - (#eq? @name "bindings") -)"#; - -/// [ "editor::FoldAtLevel", { "level": 1 } ] -> [ "editor::FoldAtLevel", 1 ] -fn replace_action_argument_object_with_single_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - let action_key_ix = query.capture_index_for_name("action_key")?; - 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 action_key = contents.get( - mat.nodes_for_capture_index(action_key_ix) - .next()? - .byte_range(), - )?; - let argument = contents.get( - mat.nodes_for_capture_index(argument_ix) - .next()? - .byte_range(), - )?; - - let new_action_name = UNWRAP_OBJECTS.get(&action_name)?.get(&action_key)?; - - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - let replacement = format!("[\"{}\", {}]", new_action_name, argument); - Some((range_to_replace, replacement)) -} - -/// "ctrl-k ctrl-1": [ "editor::PushOperator", { "Object": {} } ] -> [ "editor::vim::PushObject", {} ] -static UNWRAP_OBJECTS: LazyLock>> = 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"), - ]), - ), - ]) -}); - -const KEYMAP_MIGRATION_REPLACEMENT_PATTERNS: MigrationPatterns = &[( - ACTION_ARGUMENT_SNAKE_CASE_PATTERN, - action_argument_snake_case, -)]; - -static KEYMAP_MIGRATION_REPLACEMENT_QUERY: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - &KEYMAP_MIGRATION_REPLACEMENT_PATTERNS - .iter() - .map(|pattern| pattern.0) - .collect::(), - ) - .unwrap() -}); - -const 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") -)"#; - -fn replace_string_action( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, 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 -} - -/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu" -static STRING_REPLACE: LazyLock> = 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", - ), - ( - "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)>> = - 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)]), - ), - ), - ]) - }); - -const CONTEXT_PREDICATE_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: (string (string_content) @context_predicate) - ) - ) - ) - (#eq? @name "context") -)"#; - -fn rename_context_key( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, 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.to_string())) - } else { - None - } -} - -/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions" -pub static CONTEXT_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ("inline_completion", "edit_prediction"), - ( - "inline_completion_requires_modifier", - "edit_prediction_requires_modifier", - ), - ]) -}); - -const ACTION_ARGUMENT_SNAKE_CASE_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") -)"#; - -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, 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() + .unwrap() + }); }; - - 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)) } -pub static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock> = - 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"), - ]) - }); +// keymap +define_query!( + KEYMAP_QUERY_2025_01_29, + migrations::m_2025_01_29::KEYMAP_PATTERNS +); +define_query!( + KEYMAP_QUERY_2025_01_30, + migrations::m_2025_01_30::KEYMAP_PATTERNS +); +define_query!( + KEYMAP_QUERY_2025_03_03, + migrations::m_2025_03_03::KEYMAP_PATTERNS +); +define_query!( + KEYMAP_QUERY_2025_03_06, + migrations::m_2025_03_06::KEYMAP_PATTERNS +); -const SETTINGS_MIGRATION_PATTERNS: MigrationPatterns = &[ - (SETTINGS_STRING_REPLACE_QUERY, replace_setting_name), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_edit_prediction_provider_setting, - ), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_tab_close_button_setting_key, - ), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_tab_close_button_setting_value, - ), - ( - SETTINGS_REPLACE_IN_LANGUAGES_QUERY, - replace_setting_in_languages, - ), -]; - -static SETTINGS_MIGRATION_QUERY: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - &SETTINGS_MIGRATION_PATTERNS - .iter() - .map(|pattern| pattern.0) - .collect::(), - ) - .unwrap() -}); +// settings +define_query!( + SETTINGS_QUERY_2025_01_29, + migrations::m_2025_01_29::SETTINGS_PATTERNS +); +define_query!( + SETTINGS_QUERY_2025_01_30, + migrations::m_2025_01_30::SETTINGS_PATTERNS +); +// custom query static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock = LazyLock::new(|| { Query::new( &tree_sitter_json::LANGUAGE.into(), @@ -653,199 +184,6 @@ static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock = LazyLock::new .unwrap() }); -const SETTINGS_STRING_REPLACE_QUERY: &str = r#"(document - (object - (pair - key: (string (string_content) @name) - value: (_) - ) - ) -)"#; - -fn replace_setting_name( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, 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> = - 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"), - ]) - }); - -const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#" -(object - (pair - key: (string (string_content) @parent_key) - value: (object - (pair - key: (string (string_content) @setting_name) - value: (_) @setting_value - ) - ) - ) -) -"#; - -fn replace_edit_prediction_provider_setting( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, 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.clone())?; - - 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_tab_close_button_setting_key( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, 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.clone())?; - - 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, 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.clone())?; - - 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.clone())?; - - 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 -} - -const SETTINGS_REPLACE_IN_LANGUAGES_QUERY: &str = r#" -(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") -"#; - -fn replace_setting_in_languages( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, 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> = - LazyLock::new(|| { - HashMap::from_iter([ - ("show_inline_completions", "show_edit_predictions"), - ( - "inline_completions_disabled_in", - "edit_predictions_disabled_in", - ), - ]) - }); - #[cfg(test)] mod tests { use super::*; @@ -987,14 +325,17 @@ mod tests { } #[test] - fn test_string_to_array_replace() { + fn test_incremental_migrations() { + // Here string transforms to array internally. Then, that array transforms back to string. assert_migrate_keymap( r#" [ { "bindings": { - "ctrl-q": "editor::GoToHunk", - "ctrl-w": "editor::GoToPrevHunk" + "ctrl-q": "editor::GoToHunk", // should remain same + "ctrl-w": "editor::GoToPrevHunk", // should rename + "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform + "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform } } ] @@ -1004,8 +345,10 @@ mod tests { [ { "bindings": { - "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], - "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] + "ctrl-q": "editor::GoToHunk", // should remain same + "ctrl-w": "editor::GoToPreviousHunk", // should rename + "ctrl-q": "editor::GoToHunk", // should transform + "ctrl-w": "editor::GoToPreviousHunk" // should transform } } ] diff --git a/crates/migrator/src/patterns.rs b/crates/migrator/src/patterns.rs new file mode 100644 index 0000000000..b793ac51b9 --- /dev/null +++ b/crates/migrator/src/patterns.rs @@ -0,0 +1,11 @@ +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_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN, +}; diff --git a/crates/migrator/src/patterns/keymap.rs b/crates/migrator/src/patterns/keymap.rs new file mode 100644 index 0000000000..439c10fd4a --- /dev/null +++ b/crates/migrator/src/patterns/keymap.rs @@ -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") +)"#; diff --git a/crates/migrator/src/patterns/settings.rs b/crates/migrator/src/patterns/settings.rs new file mode 100644 index 0000000000..107b781699 --- /dev/null +++ b/crates/migrator/src/patterns/settings.rs @@ -0,0 +1,41 @@ +pub const SETTINGS_ROOT_KEY_VALUE_PATTERN: &str = r#"(document + (object + (pair + key: (string (string_content) @name) + 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") +)"#;