1//! ## When to create a migration and why?
2//! 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.).
3//!
4//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally.
5//! It also provides a quick way to migrate their existing settings to the latest state using button in UI.
6//!
7//! ## How to create a migration?
8//! 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.
9//! Once queried, *you can filter out the modified items* and write the replacement logic.
10//!
11//! You *must not* modify previous migrations; always create new ones instead.
12//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state.
13//! 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.
14//!
15//! 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.
16
17use anyhow::{Context, Result};
18use std::{cmp::Reverse, ops::Range, sync::LazyLock};
19use streaming_iterator::StreamingIterator;
20use tree_sitter::{Query, QueryMatch};
21
22use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
23
24mod migrations;
25mod patterns;
26
27fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
28 let mut parser = tree_sitter::Parser::new();
29 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
30 let syntax_tree = parser
31 .parse(&text, None)
32 .context("failed to parse settings")?;
33
34 let mut cursor = tree_sitter::QueryCursor::new();
35 let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
36
37 let mut edits = vec![];
38 while let Some(mat) = matches.next() {
39 if let Some((_, callback)) = patterns.get(mat.pattern_index) {
40 edits.extend(callback(&text, &mat, query));
41 }
42 }
43
44 edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
45 edits.dedup_by(|(range_b, _), (range_a, _)| {
46 range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
47 });
48
49 if edits.is_empty() {
50 Ok(None)
51 } else {
52 let mut new_text = text.to_string();
53 for (range, replacement) in edits.iter().rev() {
54 new_text.replace_range(range.clone(), replacement);
55 }
56 if new_text == text {
57 log::error!(
58 "Edits computed for configuration migration do not cause a change: {:?}",
59 edits
60 );
61 Ok(None)
62 } else {
63 Ok(Some(new_text))
64 }
65 }
66}
67
68fn run_migrations(
69 text: &str,
70 migrations: &[(MigrationPatterns, &Query)],
71) -> Result<Option<String>> {
72 let mut current_text = text.to_string();
73 let mut result: Option<String> = None;
74 for (patterns, query) in migrations.iter() {
75 if let Some(migrated_text) = migrate(¤t_text, patterns, query)? {
76 current_text = migrated_text.clone();
77 result = Some(migrated_text);
78 }
79 }
80 Ok(result.filter(|new_text| text != new_text))
81}
82
83pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
84 let migrations: &[(MigrationPatterns, &Query)] = &[
85 (
86 migrations::m_2025_01_29::KEYMAP_PATTERNS,
87 &KEYMAP_QUERY_2025_01_29,
88 ),
89 (
90 migrations::m_2025_01_30::KEYMAP_PATTERNS,
91 &KEYMAP_QUERY_2025_01_30,
92 ),
93 (
94 migrations::m_2025_03_03::KEYMAP_PATTERNS,
95 &KEYMAP_QUERY_2025_03_03,
96 ),
97 (
98 migrations::m_2025_03_06::KEYMAP_PATTERNS,
99 &KEYMAP_QUERY_2025_03_06,
100 ),
101 ];
102 run_migrations(text, migrations)
103}
104
105pub fn migrate_settings(text: &str) -> Result<Option<String>> {
106 let migrations: &[(MigrationPatterns, &Query)] = &[
107 (
108 migrations::m_2025_01_29::SETTINGS_PATTERNS,
109 &SETTINGS_QUERY_2025_01_29,
110 ),
111 (
112 migrations::m_2025_01_30::SETTINGS_PATTERNS,
113 &SETTINGS_QUERY_2025_01_30,
114 ),
115 ];
116 run_migrations(text, migrations)
117}
118
119pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
120 migrate(
121 &text,
122 &[(
123 SETTINGS_NESTED_KEY_VALUE_PATTERN,
124 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
125 )],
126 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
127 )
128}
129
130pub type MigrationPatterns = &'static [(
131 &'static str,
132 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
133)];
134
135macro_rules! define_query {
136 ($var_name:ident, $patterns_path:path) => {
137 static $var_name: LazyLock<Query> = LazyLock::new(|| {
138 Query::new(
139 &tree_sitter_json::LANGUAGE.into(),
140 &$patterns_path
141 .iter()
142 .map(|pattern| pattern.0)
143 .collect::<String>(),
144 )
145 .unwrap()
146 });
147 };
148}
149
150// keymap
151define_query!(
152 KEYMAP_QUERY_2025_01_29,
153 migrations::m_2025_01_29::KEYMAP_PATTERNS
154);
155define_query!(
156 KEYMAP_QUERY_2025_01_30,
157 migrations::m_2025_01_30::KEYMAP_PATTERNS
158);
159define_query!(
160 KEYMAP_QUERY_2025_03_03,
161 migrations::m_2025_03_03::KEYMAP_PATTERNS
162);
163define_query!(
164 KEYMAP_QUERY_2025_03_06,
165 migrations::m_2025_03_06::KEYMAP_PATTERNS
166);
167
168// settings
169define_query!(
170 SETTINGS_QUERY_2025_01_29,
171 migrations::m_2025_01_29::SETTINGS_PATTERNS
172);
173define_query!(
174 SETTINGS_QUERY_2025_01_30,
175 migrations::m_2025_01_30::SETTINGS_PATTERNS
176);
177
178// custom query
179static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
180 Query::new(
181 &tree_sitter_json::LANGUAGE.into(),
182 SETTINGS_NESTED_KEY_VALUE_PATTERN,
183 )
184 .unwrap()
185});
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
192 let migrated = migrate_keymap(&input).unwrap();
193 pretty_assertions::assert_eq!(migrated.as_deref(), output);
194 }
195
196 fn assert_migrate_settings(input: &str, output: Option<&str>) {
197 let migrated = migrate_settings(&input).unwrap();
198 pretty_assertions::assert_eq!(migrated.as_deref(), output);
199 }
200
201 #[test]
202 fn test_replace_array_with_single_string() {
203 assert_migrate_keymap(
204 r#"
205 [
206 {
207 "bindings": {
208 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
209 }
210 }
211 ]
212 "#,
213 Some(
214 r#"
215 [
216 {
217 "bindings": {
218 "cmd-1": "workspace::ActivatePaneUp"
219 }
220 }
221 ]
222 "#,
223 ),
224 )
225 }
226
227 #[test]
228 fn test_replace_action_argument_object_with_single_value() {
229 assert_migrate_keymap(
230 r#"
231 [
232 {
233 "bindings": {
234 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
235 }
236 }
237 ]
238 "#,
239 Some(
240 r#"
241 [
242 {
243 "bindings": {
244 "cmd-1": ["editor::FoldAtLevel", 1]
245 }
246 }
247 ]
248 "#,
249 ),
250 )
251 }
252
253 #[test]
254 fn test_replace_action_argument_object_with_single_value_2() {
255 assert_migrate_keymap(
256 r#"
257 [
258 {
259 "bindings": {
260 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
261 }
262 }
263 ]
264 "#,
265 Some(
266 r#"
267 [
268 {
269 "bindings": {
270 "cmd-1": ["vim::PushObject", { "some" : "value" }]
271 }
272 }
273 ]
274 "#,
275 ),
276 )
277 }
278
279 #[test]
280 fn test_rename_string_action() {
281 assert_migrate_keymap(
282 r#"
283 [
284 {
285 "bindings": {
286 "cmd-1": "inline_completion::ToggleMenu"
287 }
288 }
289 ]
290 "#,
291 Some(
292 r#"
293 [
294 {
295 "bindings": {
296 "cmd-1": "edit_prediction::ToggleMenu"
297 }
298 }
299 ]
300 "#,
301 ),
302 )
303 }
304
305 #[test]
306 fn test_rename_context_key() {
307 assert_migrate_keymap(
308 r#"
309 [
310 {
311 "context": "Editor && inline_completion && !showing_completions"
312 }
313 ]
314 "#,
315 Some(
316 r#"
317 [
318 {
319 "context": "Editor && edit_prediction && !showing_completions"
320 }
321 ]
322 "#,
323 ),
324 )
325 }
326
327 #[test]
328 fn test_incremental_migrations() {
329 // Here string transforms to array internally. Then, that array transforms back to string.
330 assert_migrate_keymap(
331 r#"
332 [
333 {
334 "bindings": {
335 "ctrl-q": "editor::GoToHunk", // should remain same
336 "ctrl-w": "editor::GoToPrevHunk", // should rename
337 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
338 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
339 }
340 }
341 ]
342 "#,
343 Some(
344 r#"
345 [
346 {
347 "bindings": {
348 "ctrl-q": "editor::GoToHunk", // should remain same
349 "ctrl-w": "editor::GoToPreviousHunk", // should rename
350 "ctrl-q": "editor::GoToHunk", // should transform
351 "ctrl-w": "editor::GoToPreviousHunk" // should transform
352 }
353 }
354 ]
355 "#,
356 ),
357 )
358 }
359
360 #[test]
361 fn test_action_argument_snake_case() {
362 // First performs transformations, then replacements
363 assert_migrate_keymap(
364 r#"
365 [
366 {
367 "bindings": {
368 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
369 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
370 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
371 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
372 }
373 }
374 ]
375 "#,
376 Some(
377 r#"
378 [
379 {
380 "bindings": {
381 "cmd-1": ["vim::PushObject", { "around": false }],
382 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
383 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
384 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
385 }
386 }
387 ]
388 "#,
389 ),
390 )
391 }
392
393 #[test]
394 fn test_replace_setting_name() {
395 assert_migrate_settings(
396 r#"
397 {
398 "show_inline_completions_in_menu": true,
399 "show_inline_completions": true,
400 "inline_completions_disabled_in": ["string"],
401 "inline_completions": { "some" : "value" }
402 }
403 "#,
404 Some(
405 r#"
406 {
407 "show_edit_predictions_in_menu": true,
408 "show_edit_predictions": true,
409 "edit_predictions_disabled_in": ["string"],
410 "edit_predictions": { "some" : "value" }
411 }
412 "#,
413 ),
414 )
415 }
416
417 #[test]
418 fn test_nested_string_replace_for_settings() {
419 assert_migrate_settings(
420 r#"
421 {
422 "features": {
423 "inline_completion_provider": "zed"
424 },
425 }
426 "#,
427 Some(
428 r#"
429 {
430 "features": {
431 "edit_prediction_provider": "zed"
432 },
433 }
434 "#,
435 ),
436 )
437 }
438
439 #[test]
440 fn test_replace_settings_in_languages() {
441 assert_migrate_settings(
442 r#"
443 {
444 "languages": {
445 "Astro": {
446 "show_inline_completions": true
447 }
448 }
449 }
450 "#,
451 Some(
452 r#"
453 {
454 "languages": {
455 "Astro": {
456 "show_edit_predictions": true
457 }
458 }
459 }
460 "#,
461 ),
462 )
463 }
464}