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 as _, 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 migrations::m_2025_04_15::KEYMAP_PATTERNS,
103 &KEYMAP_QUERY_2025_04_15,
104 ),
105 ];
106 run_migrations(text, migrations)
107}
108
109pub fn migrate_settings(text: &str) -> Result<Option<String>> {
110 let migrations: &[(MigrationPatterns, &Query)] = &[
111 (
112 migrations::m_2025_01_02::SETTINGS_PATTERNS,
113 &SETTINGS_QUERY_2025_01_02,
114 ),
115 (
116 migrations::m_2025_01_29::SETTINGS_PATTERNS,
117 &SETTINGS_QUERY_2025_01_29,
118 ),
119 (
120 migrations::m_2025_01_30::SETTINGS_PATTERNS,
121 &SETTINGS_QUERY_2025_01_30,
122 ),
123 (
124 migrations::m_2025_03_29::SETTINGS_PATTERNS,
125 &SETTINGS_QUERY_2025_03_29,
126 ),
127 (
128 migrations::m_2025_04_15::SETTINGS_PATTERNS,
129 &SETTINGS_QUERY_2025_04_15,
130 ),
131 (
132 migrations::m_2025_04_21::SETTINGS_PATTERNS,
133 &SETTINGS_QUERY_2025_04_21,
134 ),
135 (
136 migrations::m_2025_04_23::SETTINGS_PATTERNS,
137 &SETTINGS_QUERY_2025_04_23,
138 ),
139 (
140 migrations::m_2025_05_05::SETTINGS_PATTERNS,
141 &SETTINGS_QUERY_2025_05_05,
142 ),
143 (
144 migrations::m_2025_05_08::SETTINGS_PATTERNS,
145 &SETTINGS_QUERY_2025_05_08,
146 ),
147 (
148 migrations::m_2025_05_29::SETTINGS_PATTERNS,
149 &SETTINGS_QUERY_2025_05_29,
150 ),
151 ];
152 run_migrations(text, migrations)
153}
154
155pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
156 migrate(
157 &text,
158 &[(
159 SETTINGS_NESTED_KEY_VALUE_PATTERN,
160 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
161 )],
162 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
163 )
164}
165
166pub type MigrationPatterns = &'static [(
167 &'static str,
168 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
169)];
170
171macro_rules! define_query {
172 ($var_name:ident, $patterns_path:path) => {
173 static $var_name: LazyLock<Query> = LazyLock::new(|| {
174 Query::new(
175 &tree_sitter_json::LANGUAGE.into(),
176 &$patterns_path
177 .iter()
178 .map(|pattern| pattern.0)
179 .collect::<String>(),
180 )
181 .unwrap()
182 });
183 };
184}
185
186// keymap
187define_query!(
188 KEYMAP_QUERY_2025_01_29,
189 migrations::m_2025_01_29::KEYMAP_PATTERNS
190);
191define_query!(
192 KEYMAP_QUERY_2025_01_30,
193 migrations::m_2025_01_30::KEYMAP_PATTERNS
194);
195define_query!(
196 KEYMAP_QUERY_2025_03_03,
197 migrations::m_2025_03_03::KEYMAP_PATTERNS
198);
199define_query!(
200 KEYMAP_QUERY_2025_03_06,
201 migrations::m_2025_03_06::KEYMAP_PATTERNS
202);
203define_query!(
204 KEYMAP_QUERY_2025_04_15,
205 migrations::m_2025_04_15::KEYMAP_PATTERNS
206);
207
208// settings
209define_query!(
210 SETTINGS_QUERY_2025_01_02,
211 migrations::m_2025_01_02::SETTINGS_PATTERNS
212);
213define_query!(
214 SETTINGS_QUERY_2025_01_29,
215 migrations::m_2025_01_29::SETTINGS_PATTERNS
216);
217define_query!(
218 SETTINGS_QUERY_2025_01_30,
219 migrations::m_2025_01_30::SETTINGS_PATTERNS
220);
221define_query!(
222 SETTINGS_QUERY_2025_03_29,
223 migrations::m_2025_03_29::SETTINGS_PATTERNS
224);
225define_query!(
226 SETTINGS_QUERY_2025_04_15,
227 migrations::m_2025_04_15::SETTINGS_PATTERNS
228);
229define_query!(
230 SETTINGS_QUERY_2025_04_21,
231 migrations::m_2025_04_21::SETTINGS_PATTERNS
232);
233define_query!(
234 SETTINGS_QUERY_2025_04_23,
235 migrations::m_2025_04_23::SETTINGS_PATTERNS
236);
237define_query!(
238 SETTINGS_QUERY_2025_05_05,
239 migrations::m_2025_05_05::SETTINGS_PATTERNS
240);
241define_query!(
242 SETTINGS_QUERY_2025_05_08,
243 migrations::m_2025_05_08::SETTINGS_PATTERNS
244);
245define_query!(
246 SETTINGS_QUERY_2025_05_29,
247 migrations::m_2025_05_29::SETTINGS_PATTERNS
248);
249
250// custom query
251static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
252 Query::new(
253 &tree_sitter_json::LANGUAGE.into(),
254 SETTINGS_NESTED_KEY_VALUE_PATTERN,
255 )
256 .unwrap()
257});
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
264 let migrated = migrate_keymap(&input).unwrap();
265 pretty_assertions::assert_eq!(migrated.as_deref(), output);
266 }
267
268 fn assert_migrate_settings(input: &str, output: Option<&str>) {
269 let migrated = migrate_settings(&input).unwrap();
270 pretty_assertions::assert_eq!(migrated.as_deref(), output);
271 }
272
273 #[test]
274 fn test_replace_array_with_single_string() {
275 assert_migrate_keymap(
276 r#"
277 [
278 {
279 "bindings": {
280 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
281 }
282 }
283 ]
284 "#,
285 Some(
286 r#"
287 [
288 {
289 "bindings": {
290 "cmd-1": "workspace::ActivatePaneUp"
291 }
292 }
293 ]
294 "#,
295 ),
296 )
297 }
298
299 #[test]
300 fn test_replace_action_argument_object_with_single_value() {
301 assert_migrate_keymap(
302 r#"
303 [
304 {
305 "bindings": {
306 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
307 }
308 }
309 ]
310 "#,
311 Some(
312 r#"
313 [
314 {
315 "bindings": {
316 "cmd-1": ["editor::FoldAtLevel", 1]
317 }
318 }
319 ]
320 "#,
321 ),
322 )
323 }
324
325 #[test]
326 fn test_replace_action_argument_object_with_single_value_2() {
327 assert_migrate_keymap(
328 r#"
329 [
330 {
331 "bindings": {
332 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
333 }
334 }
335 ]
336 "#,
337 Some(
338 r#"
339 [
340 {
341 "bindings": {
342 "cmd-1": ["vim::PushObject", { "some" : "value" }]
343 }
344 }
345 ]
346 "#,
347 ),
348 )
349 }
350
351 #[test]
352 fn test_rename_string_action() {
353 assert_migrate_keymap(
354 r#"
355 [
356 {
357 "bindings": {
358 "cmd-1": "inline_completion::ToggleMenu"
359 }
360 }
361 ]
362 "#,
363 Some(
364 r#"
365 [
366 {
367 "bindings": {
368 "cmd-1": "edit_prediction::ToggleMenu"
369 }
370 }
371 ]
372 "#,
373 ),
374 )
375 }
376
377 #[test]
378 fn test_rename_context_key() {
379 assert_migrate_keymap(
380 r#"
381 [
382 {
383 "context": "Editor && inline_completion && !showing_completions"
384 }
385 ]
386 "#,
387 Some(
388 r#"
389 [
390 {
391 "context": "Editor && edit_prediction && !showing_completions"
392 }
393 ]
394 "#,
395 ),
396 )
397 }
398
399 #[test]
400 fn test_incremental_migrations() {
401 // Here string transforms to array internally. Then, that array transforms back to string.
402 assert_migrate_keymap(
403 r#"
404 [
405 {
406 "bindings": {
407 "ctrl-q": "editor::GoToHunk", // should remain same
408 "ctrl-w": "editor::GoToPrevHunk", // should rename
409 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
410 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
411 }
412 }
413 ]
414 "#,
415 Some(
416 r#"
417 [
418 {
419 "bindings": {
420 "ctrl-q": "editor::GoToHunk", // should remain same
421 "ctrl-w": "editor::GoToPreviousHunk", // should rename
422 "ctrl-q": "editor::GoToHunk", // should transform
423 "ctrl-w": "editor::GoToPreviousHunk" // should transform
424 }
425 }
426 ]
427 "#,
428 ),
429 )
430 }
431
432 #[test]
433 fn test_action_argument_snake_case() {
434 // First performs transformations, then replacements
435 assert_migrate_keymap(
436 r#"
437 [
438 {
439 "bindings": {
440 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
441 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
442 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
443 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
444 }
445 }
446 ]
447 "#,
448 Some(
449 r#"
450 [
451 {
452 "bindings": {
453 "cmd-1": ["vim::PushObject", { "around": false }],
454 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
455 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
456 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
457 }
458 }
459 ]
460 "#,
461 ),
462 )
463 }
464
465 #[test]
466 fn test_replace_setting_name() {
467 assert_migrate_settings(
468 r#"
469 {
470 "show_inline_completions_in_menu": true,
471 "show_inline_completions": true,
472 "inline_completions_disabled_in": ["string"],
473 "inline_completions": { "some" : "value" }
474 }
475 "#,
476 Some(
477 r#"
478 {
479 "show_edit_predictions_in_menu": true,
480 "show_edit_predictions": true,
481 "edit_predictions_disabled_in": ["string"],
482 "edit_predictions": { "some" : "value" }
483 }
484 "#,
485 ),
486 )
487 }
488
489 #[test]
490 fn test_nested_string_replace_for_settings() {
491 assert_migrate_settings(
492 r#"
493 {
494 "features": {
495 "inline_completion_provider": "zed"
496 },
497 }
498 "#,
499 Some(
500 r#"
501 {
502 "features": {
503 "edit_prediction_provider": "zed"
504 },
505 }
506 "#,
507 ),
508 )
509 }
510
511 #[test]
512 fn test_replace_settings_in_languages() {
513 assert_migrate_settings(
514 r#"
515 {
516 "languages": {
517 "Astro": {
518 "show_inline_completions": true
519 }
520 }
521 }
522 "#,
523 Some(
524 r#"
525 {
526 "languages": {
527 "Astro": {
528 "show_edit_predictions": true
529 }
530 }
531 }
532 "#,
533 ),
534 )
535 }
536
537 #[test]
538 fn test_replace_settings_value() {
539 assert_migrate_settings(
540 r#"
541 {
542 "scrollbar": {
543 "diagnostics": true
544 },
545 "chat_panel": {
546 "button": true
547 }
548 }
549 "#,
550 Some(
551 r#"
552 {
553 "scrollbar": {
554 "diagnostics": "all"
555 },
556 "chat_panel": {
557 "button": "always"
558 }
559 }
560 "#,
561 ),
562 )
563 }
564
565 #[test]
566 fn test_replace_settings_name_and_value() {
567 assert_migrate_settings(
568 r#"
569 {
570 "tabs": {
571 "always_show_close_button": true
572 }
573 }
574 "#,
575 Some(
576 r#"
577 {
578 "tabs": {
579 "show_close_button": "always"
580 }
581 }
582 "#,
583 ),
584 )
585 }
586
587 #[test]
588 fn test_replace_bash_with_terminal_in_profiles() {
589 assert_migrate_settings(
590 r#"
591 {
592 "assistant": {
593 "profiles": {
594 "custom": {
595 "name": "Custom",
596 "tools": {
597 "bash": true,
598 "diagnostics": true
599 }
600 }
601 }
602 }
603 }
604 "#,
605 Some(
606 r#"
607 {
608 "agent": {
609 "profiles": {
610 "custom": {
611 "name": "Custom",
612 "tools": {
613 "terminal": true,
614 "diagnostics": true
615 }
616 }
617 }
618 }
619 }
620 "#,
621 ),
622 )
623 }
624
625 #[test]
626 fn test_replace_bash_false_with_terminal_in_profiles() {
627 assert_migrate_settings(
628 r#"
629 {
630 "assistant": {
631 "profiles": {
632 "custom": {
633 "name": "Custom",
634 "tools": {
635 "bash": false,
636 "diagnostics": true
637 }
638 }
639 }
640 }
641 }
642 "#,
643 Some(
644 r#"
645 {
646 "agent": {
647 "profiles": {
648 "custom": {
649 "name": "Custom",
650 "tools": {
651 "terminal": false,
652 "diagnostics": true
653 }
654 }
655 }
656 }
657 }
658 "#,
659 ),
660 )
661 }
662
663 #[test]
664 fn test_no_bash_in_profiles() {
665 assert_migrate_settings(
666 r#"
667 {
668 "assistant": {
669 "profiles": {
670 "custom": {
671 "name": "Custom",
672 "tools": {
673 "diagnostics": true,
674 "find_path": true,
675 "read_file": true
676 }
677 }
678 }
679 }
680 }
681 "#,
682 Some(
683 r#"
684 {
685 "agent": {
686 "profiles": {
687 "custom": {
688 "name": "Custom",
689 "tools": {
690 "diagnostics": true,
691 "find_path": true,
692 "read_file": true
693 }
694 }
695 }
696 }
697 }
698 "#,
699 ),
700 )
701 }
702
703 #[test]
704 fn test_rename_path_search_to_find_path() {
705 assert_migrate_settings(
706 r#"
707 {
708 "assistant": {
709 "profiles": {
710 "default": {
711 "tools": {
712 "path_search": true,
713 "read_file": true
714 }
715 }
716 }
717 }
718 }
719 "#,
720 Some(
721 r#"
722 {
723 "agent": {
724 "profiles": {
725 "default": {
726 "tools": {
727 "find_path": true,
728 "read_file": true
729 }
730 }
731 }
732 }
733 }
734 "#,
735 ),
736 );
737 }
738
739 #[test]
740 fn test_rename_assistant() {
741 assert_migrate_settings(
742 r#"{
743 "assistant": {
744 "foo": "bar"
745 },
746 "edit_predictions": {
747 "enabled_in_assistant": false,
748 }
749 }"#,
750 Some(
751 r#"{
752 "agent": {
753 "foo": "bar"
754 },
755 "edit_predictions": {
756 "enabled_in_text_threads": false,
757 }
758 }"#,
759 ),
760 );
761 }
762
763 #[test]
764 fn test_comment_duplicated_agent() {
765 assert_migrate_settings(
766 r#"{
767 "agent": {
768 "name": "assistant-1",
769 "model": "gpt-4", // weird formatting
770 "utf8": "привіт"
771 },
772 "something": "else",
773 "agent": {
774 "name": "assistant-2",
775 "model": "gemini-pro"
776 }
777 }
778 "#,
779 Some(
780 r#"{
781 /* Duplicated key auto-commented: "agent": {
782 "name": "assistant-1",
783 "model": "gpt-4", // weird formatting
784 "utf8": "привіт"
785 }, */
786 "something": "else",
787 "agent": {
788 "name": "assistant-2",
789 "model": "gemini-pro"
790 }
791 }
792 "#,
793 ),
794 );
795 }
796
797 #[test]
798 fn test_preferred_completion_mode_migration() {
799 assert_migrate_settings(
800 r#"{
801 "agent": {
802 "preferred_completion_mode": "max",
803 "enabled": true
804 }
805 }"#,
806 Some(
807 r#"{
808 "agent": {
809 "preferred_completion_mode": "burn",
810 "enabled": true
811 }
812 }"#,
813 ),
814 );
815
816 assert_migrate_settings(
817 r#"{
818 "agent": {
819 "preferred_completion_mode": "normal",
820 "enabled": true
821 }
822 }"#,
823 None,
824 );
825
826 assert_migrate_settings(
827 r#"{
828 "agent": {
829 "preferred_completion_mode": "burn",
830 "enabled": true
831 }
832 }"#,
833 None,
834 );
835
836 assert_migrate_settings(
837 r#"{
838 "other_section": {
839 "preferred_completion_mode": "max"
840 },
841 "agent": {
842 "preferred_completion_mode": "max"
843 }
844 }"#,
845 Some(
846 r#"{
847 "other_section": {
848 "preferred_completion_mode": "max"
849 },
850 "agent": {
851 "preferred_completion_mode": "burn"
852 }
853 }"#,
854 ),
855 );
856 }
857}