migrator: Add versioned migrations (#26215)
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
This commit is contained in:
parent
6fd9708eee
commit
219d36f589
11 changed files with 948 additions and 773 deletions
27
crates/migrator/src/migrations.rs
Normal file
27
crates/migrator/src/migrations.rs
Normal file
|
@ -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;
|
||||
}
|
297
crates/migrator/src/migrations/m_2025_01_29/keymap.rs
Normal file
297
crates/migrator/src/migrations/m_2025_01_29/keymap.rs
Normal file
|
@ -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",
|
||||
),
|
||||
])
|
||||
});
|
101
crates/migrator/src/migrations/m_2025_01_29/settings.rs
Normal file
101
crates/migrator/src/migrations/m_2025_01_29/settings.rs
Normal file
|
@ -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<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.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<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",
|
||||
),
|
||||
])
|
||||
});
|
82
crates/migrator/src/migrations/m_2025_01_30/keymap.rs
Normal file
82
crates/migrator/src/migrations/m_2025_01_30/keymap.rs
Normal 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::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<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"),
|
||||
])
|
||||
});
|
83
crates/migrator/src/migrations/m_2025_01_30/settings.rs
Normal file
83
crates/migrator/src/migrations/m_2025_01_30/settings.rs
Normal file
|
@ -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<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
|
||||
}
|
75
crates/migrator/src/migrations/m_2025_03_03/keymap.rs
Normal file
75
crates/migrator/src/migrations/m_2025_03_03/keymap.rs
Normal file
|
@ -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<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)]),
|
||||
),
|
||||
),
|
||||
])
|
||||
});
|
38
crates/migrator/src/migrations/m_2025_03_06/keymap.rs
Normal file
38
crates/migrator/src/migrations/m_2025_03_06/keymap.rs
Normal file
|
@ -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<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"]));
|
|
@ -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,26 +65,55 @@ fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Opt
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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))
|
||||
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<Option<String>> {
|
||||
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<Option<String>> {
|
||||
|
@ -73,578 +121,61 @@ pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<St
|
|||
&text,
|
||||
&[(
|
||||
SETTINGS_NESTED_KEY_VALUE_PATTERN,
|
||||
replace_edit_prediction_provider_setting,
|
||||
migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
|
||||
)],
|
||||
&EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
|
||||
)
|
||||
}
|
||||
|
||||
type MigrationPatterns = &'static [(
|
||||
pub 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
|
||||
)
|
||||
)
|
||||
)
|
||||
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 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"),
|
||||
])
|
||||
});
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
/// "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"),
|
||||
]),
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// "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)
|
||||
)
|
||||
)
|
||||
)
|
||||
(#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",
|
||||
),
|
||||
])
|
||||
});
|
||||
|
||||
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<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"),
|
||||
])
|
||||
});
|
||||
// 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<Query> = LazyLock::new(|| {
|
||||
Query::new(
|
||||
&tree_sitter_json::LANGUAGE.into(),
|
||||
&SETTINGS_MIGRATION_PATTERNS
|
||||
.iter()
|
||||
.map(|pattern| pattern.0)
|
||||
.collect::<String>(),
|
||||
)
|
||||
.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<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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
11
crates/migrator/src/patterns.rs
Normal file
11
crates/migrator/src/patterns.rs
Normal file
|
@ -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,
|
||||
};
|
77
crates/migrator/src/patterns/keymap.rs
Normal file
77
crates/migrator/src/patterns/keymap.rs
Normal 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")
|
||||
)"#;
|
41
crates/migrator/src/patterns/settings.rs
Normal file
41
crates/migrator/src/patterns/settings.rs
Normal file
|
@ -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")
|
||||
)"#;
|
Loading…
Add table
Add a link
Reference in a new issue