@@ -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<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"),
+ ])
+});
+
+/// [ "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.to_string()))
+ } 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",
+ ),
+ ])
+});
@@ -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<Option<String>> {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
@@ -46,605 +65,117 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
}
}
-pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
- 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))
-}
-
-pub fn migrate_settings(text: &str) -> Result<Option<String>> {
- migrate(
- &text,
- SETTINGS_MIGRATION_PATTERNS,
- &SETTINGS_MIGRATION_QUERY,
- )
-}
-
-pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
- migrate(
- &text,
- &[(
- SETTINGS_NESTED_KEY_VALUE_PATTERN,
- replace_edit_prediction_provider_setting,
- )],
- &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
- )
-}
-
-type MigrationPatterns = &'static [(
- &'static str,
- fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, 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<Query> = LazyLock::new(|| {
- Query::new(
- &tree_sitter_json::LANGUAGE.into(),
- &KEYMAP_MIGRATION_TRANSFORMATION_PATTERNS
- .iter()
- .map(|pattern| pattern.0)
- .collect::<String>(),
- )
- .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
- )
- )
- )
- )
- )
- )
- (#eq? @name "bindings")
-)"#;
-
-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))
+fn run_migrations(
+ text: &str,
+ migrations: &[(MigrationPatterns, &Query)],
+) -> Result<Option<String>> {
+ let mut current_text = text.to_string();
+ let mut result: Option<String> = 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)
}
-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",
- ),
+pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
+ let migrations: &[(MigrationPatterns, &Query)] = &[
(
- ("vim::PushOperator", "ToggleComments"),
- "vim::PushToggleComments",
+ migrations::m_2025_01_29::KEYMAP_PATTERNS,
+ &KEYMAP_QUERY_2025_01_29,
),
- // 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",
+ migrations::m_2025_01_30::KEYMAP_PATTERNS,
+ &KEYMAP_QUERY_2025_01_30,
),
(
- ("vim::SwitchMode", "VisualBlock"),
- "vim::SwitchToVisualBlockMode",
+ migrations::m_2025_03_03::KEYMAP_PATTERNS,
+ &KEYMAP_QUERY_2025_03_03,
),
(
- ("vim::SwitchMode", "HelixNormal"),
- "vim::SwitchToHelixNormalMode",
+ migrations::m_2025_03_06::KEYMAP_PATTERNS,
+ &KEYMAP_QUERY_2025_03_06,
),
- // 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<usize>, 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))
+ ];
+ run_migrations(text, migrations)
}
-/// "ctrl-k ctrl-1": [ "editor::PushOperator", { "Object": {} } ] -> [ "editor::vim::PushObject", {} ]
-static UNWRAP_OBJECTS: LazyLock<HashMap<&str, HashMap<&str, &str>>> = LazyLock::new(|| {
- HashMap::from_iter([
+pub fn migrate_settings(text: &str) -> Result<Option<String>> {
+ let migrations: &[(MigrationPatterns, &Query)] = &[
(
- "editor::FoldAtLevel",
- HashMap::from_iter([("level", "editor::FoldAtLevel")]),
+ migrations::m_2025_01_29::SETTINGS_PATTERNS,
+ &SETTINGS_QUERY_2025_01_29,
),
(
- "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"),
- ]),
+ migrations::m_2025_01_30::SETTINGS_PATTERNS,
+ &SETTINGS_QUERY_2025_01_30,
),
- ])
-});
-
-const KEYMAP_MIGRATION_REPLACEMENT_PATTERNS: MigrationPatterns = &[(
- ACTION_ARGUMENT_SNAKE_CASE_PATTERN,
- action_argument_snake_case,
-)];
-
-static KEYMAP_MIGRATION_REPLACEMENT_QUERY: LazyLock<Query> = LazyLock::new(|| {
- Query::new(
- &tree_sitter_json::LANGUAGE.into(),
- &KEYMAP_MIGRATION_REPLACEMENT_PATTERNS
- .iter()
- .map(|pattern| pattern.0)
- .collect::<String>(),
- )
- .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<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
+ ];
+ run_migrations(text, migrations)
}
-/// "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",
- ),
- (
- "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)]),
- ),
- ),
- ])
- });
-
-const CONTEXT_PREDICATE_PATTERN: &str = r#"(document
- (array
- (object
- (pair
- key: (string (string_content) @name)
- value: (string (string_content) @context_predicate)
- )
- )
+pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
+ migrate(
+ &text,
+ &[(
+ SETTINGS_NESTED_KEY_VALUE_PATTERN,
+ migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
+ )],
+ &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
)
- (#eq? @name "context")
-)"#;
-
-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.to_string()))
- } 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",
- ),
- ])
-});
+pub type MigrationPatterns = &'static [(
+ &'static str,
+ fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
+)];
-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
- ))
- )
- )
- )
+macro_rules! define_query {
+ ($var_name:ident, $patterns_path:path) => {
+ static $var_name: LazyLock<Query> = LazyLock::new(|| {
+ Query::new(
+ &tree_sitter_json::LANGUAGE.into(),
+ &$patterns_path
+ .iter()
+ .map(|pattern| pattern.0)
+ .collect::<String>(),
)
- )
- (#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<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()
+ .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<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"),
- ])
- });
-
-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<Query> = LazyLock::new(|| {
- Query::new(
- &tree_sitter_json::LANGUAGE.into(),
- &SETTINGS_MIGRATION_PATTERNS
- .iter()
- .map(|pattern| pattern.0)
- .collect::<String>(),
- )
- .unwrap()
-});
-
+// 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
+);
+
+// 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<Query> = LazyLock::new(|| {
Query::new(
&tree_sitter_json::LANGUAGE.into(),
@@ -653,199 +184,6 @@ static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = 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<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"),
- ])
- });
-
-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<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.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<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.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<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.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<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",
- ),
- ])
- });
-
#[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
}
}
]