-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdata.js
More file actions
1734 lines (1728 loc) · 77.7 KB
/
Copy pathdata.js
File metadata and controls
1734 lines (1728 loc) · 77.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ============================================================
// VIM NINJA — Data: Lessons, Commands, Cheatsheet
// ============================================================
const LESSONS = [
// ── BEGINNER ──
{
id: 'intro-modes',
title: 'Introduction to Vim Modes',
level: 'beginner',
category: 'Basic Vim',
keys: ['modes'],
xp: 50,
desc: 'The first key Vim concept: understanding modes. Vim has multiple modes that change how keystrokes are interpreted.',
commands: [
{ key: 'i', desc: 'Enter Insert mode (before cursor)' },
{ key: 'Esc', desc: 'Return to Normal mode' },
{ key: 'a', desc: 'Enter Insert mode (after cursor)' },
],
explanations: [
{
title: '🎭 What are Modes?',
body: 'Unlike most editors, Vim is modal. Each mode serves a specific purpose. In <code class="inline">Normal</code> mode you navigate and execute commands. In <code class="inline">Insert</code> mode you type text. In <code class="inline">Visual</code> mode you select text.',
},
{
title: '⌨️ Normal Mode',
body: 'Normal mode is the default mode when you open Vim. Every key is a command — not text input. Use <code class="inline">h j k l</code> to move, <code class="inline">d</code> to delete, <code class="inline">y</code> to copy, etc.',
},
{
title: '✏️ Insert Mode',
body: 'Press <code class="inline">i</code> to enter Insert mode. Now keystrokes are typed as text — just like a regular editor. Press <code class="inline">Esc</code> to return to Normal mode.',
},
],
challenge: {
file: 'modes.txt',
initialText: 'This is Normal mode.\nPress i to enter Insert mode.\nPress Esc to go back.',
instruction: 'Press <kbd>i</kbd> to enter Insert mode, then press <kbd>Esc</kbd> to return to Normal mode. Try switching between modes 3 times.',
hint: 'Press i → type something → press Esc. Repeat!',
validate: (state) => state.modeChanges >= 3,
},
},
{
id: 'basic-movement',
title: 'Basic Movement (hjkl)',
level: 'beginner',
category: 'Basic Vim',
keys: ['h', 'j', 'k', 'l'],
xp: 60,
desc: 'Learn to move the cursor using h, j, k, l — the Vim way to navigate without touching arrow keys.',
commands: [
{ key: 'h', desc: 'Move cursor left' },
{ key: 'j', desc: 'Move cursor down' },
{ key: 'k', desc: 'Move cursor up' },
{ key: 'l', desc: 'Move cursor right' },
{ key: '5j', desc: 'Move 5 lines down (count prefix)' },
],
explanations: [
{
title: '🕹️ The Vim Home Row',
body: '<code class="inline">h j k l</code> correspond to left, down, up, right. Your right hand sits on these keys naturally on a keyboard. Keep your fingers on the home row for maximum speed!',
},
{
title: '🔢 Count Prefixes',
body: 'Prefix any motion with a number to repeat it: <code class="inline">5j</code> moves down 5 lines, <code class="inline">10l</code> moves right 10 characters. This is one of Vim\'s most powerful features.',
},
],
challenge: {
file: 'movement.txt',
initialText: 'Line one\nLine two\nLine three\nLine four\nLine five\nLine six\nLine seven\nLine eight',
instruction: 'Use <kbd>j</kbd> and <kbd>k</kbd> to move the cursor down to line 5, then back up to line 2.',
hint: 'Press j to go down, k to go up. Count the lines!',
validate: (state) => state.row <= 1 && state.maxRow >= 4,
},
},
{
id: 'moving-words',
title: 'Moving by Words',
level: 'beginner',
category: 'Basic Vim',
keys: ['w', 'e', 'b'],
xp: 60,
desc: 'Jump through text word by word for fast horizontal navigation.',
commands: [
{ key: 'w', desc: 'Jump to start of next word' },
{ key: 'e', desc: 'Jump to end of current/next word' },
{ key: 'b', desc: 'Jump back to start of previous word' },
{ key: 'W', desc: 'Jump to start of next WORD (ignores punctuation)' },
{ key: 'E', desc: 'Jump to end of next WORD (ignores punctuation)' },
{ key: 'B', desc: 'Jump back to start of previous WORD' },
],
explanations: [
{
title: '📝 word vs WORD',
body: 'A lowercase <code class="inline">word</code> is separated by whitespace or punctuation. An uppercase <code class="inline">WORD</code> is separated only by whitespace. So <code class="inline">foo.bar</code> is three words but one WORD.',
},
],
challenge: {
file: 'words.txt',
initialText: 'The quick brown fox jumps over the lazy dog.\nconst result = calculateValue(input, options);',
instruction: 'Use <kbd>w</kbd> to jump forward 5 words, then <kbd>b</kbd> to jump back 3 words.',
hint: 'w moves forward one word, b moves backward one word.',
validate: (state) => state.wordJumps >= 5,
},
},
{
id: 'insert-mode',
title: 'Insert Mode Commands',
level: 'beginner',
category: 'Insert Like a Pro',
keys: ['i', 'a', 'I', 'A', 'o', 'O'],
xp: 70,
desc: 'Six different ways to enter Insert mode — each placing the cursor in exactly the right position.',
commands: [
{ key: 'i', desc: 'Insert before cursor' },
{ key: 'a', desc: 'Insert (append) after cursor' },
{ key: 'I', desc: 'Insert at beginning of line' },
{ key: 'A', desc: 'Insert at end of line' },
{ key: 'o', desc: 'Open new line below and insert' },
{ key: 'O', desc: 'Open new line above and insert' },
{ key: 'ea', desc: 'Append at end of word' },
],
explanations: [
{
title: '⚡ Entering Insert Mode Efficiently',
body: 'Instead of navigating to a position and pressing <code class="inline">i</code>, use the right insertion command directly. <code class="inline">A</code> to add at end of line, <code class="inline">o</code> to start a new line below — saves keystrokes!',
},
],
challenge: {
file: 'insert.txt',
initialText: 'Hello World\nVim is powerful',
instruction: 'Press <kbd>A</kbd> to insert at end of line and type "!" — then press <kbd>Esc</kbd>. Then press <kbd>o</kbd> to open a new line below and type your text.',
hint: 'A goes to end of line in insert mode. o creates a new line below.',
validate: (state) => state.text.includes('!') && state.lines.length > 2,
},
},
{
id: 'delete-commands',
title: 'Delete, Cut & Change',
level: 'beginner',
category: 'Basic Operators',
keys: ['dd', 'dw', 'D', 'x', 'r', 's', 'cc', 'cw'],
xp: 80,
desc: 'Delete characters, words, and lines. The d and c operators are your most-used editing tools.',
commands: [
{ key: 'x', desc: 'Delete character under cursor' },
{ key: 'X', desc: 'Delete character before cursor (like backspace)' },
{ key: 'r', desc: 'Replace single character under cursor (press key next)' },
{ key: 's', desc: 'Delete character and enter Insert mode (substitute)' },
{ key: 'dd', desc: 'Delete (cut) current line' },
{ key: '5dd', desc: 'Delete 5 lines' },
{ key: 'dw', desc: 'Delete to start of next word' },
{ key: 'de', desc: 'Delete to end of word' },
{ key: 'db', desc: 'Delete to start of previous word' },
{ key: 'd$', desc: 'Delete to end of line' },
{ key: 'd0', desc: 'Delete to beginning of line' },
{ key: 'dG', desc: 'Delete to end of file' },
{ key: 'dgg', desc: 'Delete to beginning of file' },
{ key: 'D', desc: 'Delete to end of line (same as d$)' },
{ key: 'cc', desc: 'Delete line and enter Insert mode' },
{ key: 'cw', desc: 'Delete word and enter Insert mode' },
{ key: 'C', desc: 'Delete to end of line and enter Insert mode' },
{ key: 'ciw', desc: 'Change inner word' },
],
explanations: [
{
title: '✂️ Delete vs Change',
body: 'The <code class="inline">d</code> operator deletes text (cuts it — goes to register). The <code class="inline">c</code> operator deletes text AND enters Insert mode, ready for new text. Use <code class="inline">c</code> when you want to replace something.',
},
{
title: '🔄 Everything is Cut',
body: 'Deleted text goes to the default register. You can paste it back with <code class="inline">p</code>. Nothing is truly "lost" — it\'s always in a register.',
},
{
title: '✏️ Sizing down edits with r and s',
body: 'Instead of entering insert mode just to replace one character, press <code class="inline">r</code> followed by the new character. To replace a character and keep typing, use <code class="inline">s</code> (substitute), which deletes the character under the cursor and places you in insert mode.',
},
],
challenge: {
file: 'delete.txt',
initialText: 'Keep this line.\nDelete this entire line.\nKeep me too.\nRemove this word here.\nFinal line stays.',
instruction: 'Move to line 2 and press <kbd>dd</kbd> to delete it. Then move to "word" on line 4 and press <kbd>dw</kbd> to delete that word.',
hint: 'Use j to move down, then dd to delete the whole line.',
validate: (state) => !state.text.includes('Delete this entire line'),
},
},
{
id: 'copy-paste',
title: 'Copy (Yank) & Paste',
level: 'beginner',
category: 'Basic Operators',
keys: ['yy', 'yw', 'p', 'P'],
xp: 70,
desc: 'Yank (copy) text and paste it. "Yank" is Vim\'s term for copy — because y looks like a lasso throwing text into a register.',
commands: [
{ key: 'yy', desc: 'Yank (copy) current line' },
{ key: '3yy', desc: 'Yank 3 lines' },
{ key: 'yw', desc: 'Yank to end of word' },
{ key: 'y$', desc: 'Yank to end of line' },
{ key: 'y0', desc: 'Yank to beginning of line' },
{ key: 'yG', desc: 'Yank to end of file' },
{ key: 'yiw', desc: 'Yank inner word' },
{ key: 'p', desc: 'Paste after cursor / below current line' },
{ key: 'P', desc: 'Paste before cursor / above current line' },
{ key: '3p', desc: 'Paste 3 times' },
],
explanations: [
{
title: '📋 Registers',
body: 'When you yank, text goes to the unnamed register <code class="inline">"</code>. You can also use named registers <code class="inline">"ayy</code> (yank to register a), <code class="inline">"ap</code> (paste from a). Register <code class="inline">0</code> always has the last yank.',
},
],
challenge: {
file: 'yank.txt',
initialText: 'Original line one\nOriginal line two\nOriginal line three\n\n\n',
instruction: 'Move to line 1, press <kbd>yy</kbd> to yank it, then move down and press <kbd>p</kbd> to paste it below.',
hint: 'yy copies the current line. p pastes it below.',
validate: (state) => (state.text.match(/Original line one/g) || []).length >= 2,
},
},
{
id: 'undo-redo',
title: 'Undo & Redo',
level: 'beginner',
category: 'Basic Vim',
keys: ['u', 'Ctrl+r'],
xp: 50,
desc: 'Unlimited undo and redo. Vim keeps a full undo history — never lose your work.',
commands: [
{ key: 'u', desc: 'Undo last change' },
{ key: 'U', desc: 'Undo all changes on current line' },
{ key: 'Ctrl+r', desc: 'Redo (undo the undo)' },
{ key: '5u', desc: 'Undo 5 changes' },
],
explanations: [
{
title: '⏪ Unlimited Undo',
body: 'Vim\'s undo is extremely powerful. You can undo every single change all the way back to when you opened the file. <code class="inline">u</code> undoes one change, <code class="inline">Ctrl+r</code> redoes it.',
},
{
title: '💡 Undo Tree',
body: 'Vim actually maintains an undo tree (not just a linear history). The <code class="inline">:undolist</code> command shows all branches. Plugins like undotree.vim visualize this.',
},
],
challenge: {
file: 'undo.txt',
initialText: 'This is the original text.\nDo not change this line.',
instruction: 'Make some changes (press <kbd>i</kbd>, type text, press <kbd>Esc</kbd>), then press <kbd>u</kbd> to undo your change.',
hint: 'Press i to enter insert, type something, Esc, then u to undo.',
validate: (state) => state.undoCount >= 1,
},
},
{
id: 'search',
title: 'Search & Navigation',
level: 'beginner',
category: 'Search',
keys: ['/', '?', 'n', 'N'],
xp: 80,
desc: 'Search forward and backward through your file. The most efficient way to navigate large files.',
commands: [
{ key: '/pattern', desc: 'Search forward for pattern' },
{ key: '?pattern', desc: 'Search backward for pattern' },
{ key: 'n', desc: 'Repeat search in same direction' },
{ key: 'N', desc: 'Repeat search in opposite direction' },
{ key: '*', desc: 'Search forward for word under cursor' },
{ key: '#', desc: 'Search backward for word under cursor' },
{ key: ':noh', desc: 'Remove search highlighting' },
{ key: '/\\<word\\>', desc: 'Search for exact whole word' },
],
explanations: [
{
title: '🔍 Incremental Search',
body: 'With <code class="inline">:set incsearch</code> in your vimrc, Vim highlights matches as you type. <code class="inline">:set hlsearch</code> highlights all matches after the search.',
},
],
challenge: {
file: 'search.txt',
initialText: 'The cat sat on the mat.\nThe cat is fat.\nThe cat wore a hat.\nWhere is the cat at?\nThe cat and the bat.',
instruction: 'Type <kbd>/</kbd>cat and press <kbd>Enter</kbd> to search. Then press <kbd>n</kbd> to jump to the next match.',
hint: 'Type /cat then Enter. n goes to next match.',
validate: (state) => state.searchCount >= 2,
},
},
{
id: 'visual-mode',
title: 'Visual Mode',
level: 'beginner',
category: 'Visual Mode',
keys: ['v', 'V', 'Ctrl+v'],
xp: 80,
desc: 'Select text visually before applying an operator. Visual, Visual Line, and Visual Block modes.',
commands: [
{ key: 'v', desc: 'Enter Visual mode (character selection)' },
{ key: 'V', desc: 'Enter Visual Line mode (line selection)' },
{ key: 'Ctrl+v', desc: 'Enter Visual Block mode (column selection)' },
{ key: 'o', desc: 'In Visual mode: move to other end of selection' },
{ key: 'O', desc: 'In Visual Block: move to other corner' },
{ key: 'Esc', desc: 'Exit Visual mode' },
{ key: 'gv', desc: 'Reselect last visual selection' },
{ key: 'd', desc: 'In Visual: delete selected text' },
{ key: 'y', desc: 'In Visual: yank selected text' },
{ key: 'c', desc: 'In Visual: change selected text' },
{ key: '>', desc: 'In Visual: indent selection right' },
{ key: '<', desc: 'In Visual: indent selection left' },
{ key: '~', desc: 'In Visual: toggle case of selection' },
],
explanations: [
{
title: '🎨 Three Visual Modes',
body: '<code class="inline">v</code> selects characters, <code class="inline">V</code> selects whole lines, <code class="inline">Ctrl+v</code> selects a rectangular block (super useful for column editing). After selecting, apply any operator (d, y, c, >, <, ~).',
},
],
challenge: {
file: 'visual.txt',
initialText: 'Select and delete this: [REMOVE THIS TEXT]\nKeep this line intact.\nAnother line here.',
instruction: 'Use <kbd>v</kbd> to enter Visual mode, then select some text and press <kbd>d</kbd> to delete it.',
hint: 'Press v, move the cursor to select text, then press d to delete.',
validate: (state) => state.visualUsed === true,
},
},
{
id: 'search-replace',
title: 'Search & Replace',
level: 'beginner',
category: 'Search',
keys: [':%s'],
xp: 90,
desc: 'The power of substitution: replace any text pattern across your entire file or a range of lines.',
commands: [
{ key: ':%s/old/new/g', desc: 'Replace all occurrences in file' },
{ key: ':%s/old/new/gc', desc: 'Replace all with confirmation' },
{ key: ':%s/old/new/gi', desc: 'Replace all, case insensitive' },
{ key: ':s/old/new/g', desc: 'Replace on current line' },
{ key: ':5,10s/old/new/g', desc: 'Replace in lines 5-10' },
{ key: ":'<,'>s/old/new/g", desc: 'Replace in visual selection' },
{ key: ':%s/\\<word\\>/new/g', desc: 'Replace whole words only' },
{ key: ':%s/foo/bar/gI', desc: 'Replace, force case sensitive' },
],
explanations: [
{
title: '🔄 Substitution Syntax',
body: 'The format is <code class="inline">:range s/pattern/replacement/flags</code>. Range: <code class="inline">%</code> means whole file, <code class="inline">.</code> means current line, <code class="inline">1,5</code> means lines 1-5. Flags: <code class="inline">g</code> = global, <code class="inline">c</code> = confirm, <code class="inline">i</code> = ignore case.',
},
],
challenge: {
file: 'replace.txt',
initialText: 'Hello World! Hello Vim! Hello Everyone!\nHello is a great word.\nSay Hello to Vim Ninja.',
instruction: 'Type <kbd>:</kbd>%s/Hello/Hi/g and press Enter to replace all "Hello" with "Hi".',
hint: 'Type :%s/Hello/Hi/g then press Enter',
validate: (state) => !state.text.includes('Hello') && state.text.includes('Hi'),
},
},
// ── INTERMEDIATE ──
{
id: 'line-navigation',
title: 'Line Navigation',
level: 'intermediate',
category: 'Essential Motions',
keys: ['0', '$', '^', 'g_'],
xp: 70,
desc: 'Navigate to specific positions within a line efficiently.',
commands: [
{ key: '0', desc: 'Jump to start of line (column 0)' },
{ key: '^', desc: 'Jump to first non-blank character of line' },
{ key: '$', desc: 'Jump to end of line' },
{ key: 'g_', desc: 'Jump to last non-blank character of line' },
{ key: 'gm', desc: 'Jump to middle of screen line' },
{ key: 'n|', desc: 'Jump to column n on current line' },
],
explanations: [
{
title: '📍 Line Start Difference',
body: '<code class="inline">0</code> goes to column 0 (absolute start). <code class="inline">^</code> goes to the first non-whitespace character — much more useful when code is indented!',
},
],
challenge: {
file: 'lines.txt',
initialText: ' function hello() {\n console.log("vim");\n }\n',
instruction: 'Move the cursor to the first non-blank character using <kbd>^</kbd>, then to end of line using <kbd>$</kbd>.',
hint: '^goes to first non-blank character. $ goes to end of line.',
validate: (state) => state.lineNavUsed === true,
},
},
{
id: 'file-navigation',
title: 'File Navigation',
level: 'intermediate',
category: 'Essential Motions',
keys: ['gg', 'G', 'Ctrl+f', 'Ctrl+b'],
xp: 70,
desc: 'Navigate to any part of a file instantly — top, bottom, specific line, or half-page jumps.',
commands: [
{ key: 'gg', desc: 'Go to first line of file' },
{ key: 'G', desc: 'Go to last line of file' },
{ key: '5G', desc: 'Go to line 5 (or :5)' },
{ key: ':42', desc: 'Go to line 42' },
{ key: 'Ctrl+f', desc: 'Page forward (down) one full screen' },
{ key: 'Ctrl+b', desc: 'Page backward (up) one full screen' },
{ key: 'Ctrl+d', desc: 'Scroll down half a screen' },
{ key: 'Ctrl+u', desc: 'Scroll up half a screen' },
{ key: 'Ctrl+e', desc: 'Scroll screen down one line (cursor stays)' },
{ key: 'Ctrl+y', desc: 'Scroll screen up one line (cursor stays)' },
{ key: 'H', desc: 'Move to top of visible screen' },
{ key: 'M', desc: 'Move to middle of visible screen' },
{ key: 'L', desc: 'Move to bottom of visible screen' },
{ key: 'zz', desc: 'Center current line on screen' },
{ key: 'zt', desc: 'Scroll current line to top of screen' },
{ key: 'zb', desc: 'Scroll current line to bottom of screen' },
],
explanations: [
{
title: '🚀 Fast File Navigation',
body: '<code class="inline">gg</code> and <code class="inline">G</code> are the most used. For large files, use <code class="inline">:linenumber</code> to jump to any line instantly. <code class="inline">Ctrl+d</code> / <code class="inline">Ctrl+u</code> for half-page scrolling.',
},
],
challenge: {
file: 'navigation.txt',
initialText: Array.from({length: 20}, (_, i) => `Line ${i+1}: some content here`).join('\n'),
instruction: 'Press <kbd>G</kbd> to jump to the last line, then <kbd>gg</kbd> to jump back to the top.',
hint: 'G goes to end, gg goes to beginning.',
validate: (state) => state.usedGG === true,
},
},
{
id: 'find-char',
title: 'Find Character (f, t)',
level: 'intermediate',
category: 'Essential Motions',
keys: ['f', 't', 'F', 'T', ';', ','],
xp: 80,
desc: 'Jump to any character on the current line — one of the most precise navigation motions.',
commands: [
{ key: 'fx', desc: 'Jump to next occurrence of character x on line' },
{ key: 'Fx', desc: 'Jump to previous occurrence of character x on line' },
{ key: 'tx', desc: 'Jump to just before next occurrence of x' },
{ key: 'Tx', desc: 'Jump to just after previous occurrence of x' },
{ key: ';', desc: 'Repeat last f/t/F/T motion' },
{ key: ',', desc: 'Repeat last f/t/F/T motion in reverse' },
],
explanations: [
{
title: '🎯 Precision Navigation',
body: '<code class="inline">f</code> (find) lands ON the character. <code class="inline">t</code> (till) lands BEFORE the character. Combine with operators: <code class="inline">dfx</code> deletes up to and including character x. <code class="inline">ctx</code> changes up to (not including) x.',
},
],
challenge: {
file: 'find.txt',
initialText: 'const result = calculateValue(input, options, config);\ndelete me: remove this word: done.',
instruction: 'Use <kbd>f(</kbd> to jump to the opening parenthesis on line 1.',
hint: 'Press f then ( to jump to the next parenthesis.',
validate: (state) => state.findUsed === true,
},
},
{
id: 'text-objects',
title: 'Text Objects',
level: 'intermediate',
category: 'Text Objects',
keys: ['iw', 'aw', 'i{', 'a{'],
xp: 100,
desc: 'Text objects let you operate on semantic units of text — words, sentences, paragraphs, blocks.',
commands: [
{ key: 'iw', desc: 'Inner word (word without surrounding space)' },
{ key: 'aw', desc: 'A word (word plus surrounding space)' },
{ key: 'iW', desc: 'Inner WORD' },
{ key: 'aW', desc: 'A WORD' },
{ key: 'is', desc: 'Inner sentence' },
{ key: 'as', desc: 'A sentence' },
{ key: 'ip', desc: 'Inner paragraph' },
{ key: 'ap', desc: 'A paragraph' },
{ key: 'i(', desc: 'Inner parentheses (same as ib)' },
{ key: 'a(', desc: 'A parentheses (includes the parens)' },
{ key: 'i[', desc: 'Inner square brackets' },
{ key: 'a[', desc: 'A square brackets (includes brackets)' },
{ key: 'i{', desc: 'Inner curly braces (same as iB)' },
{ key: 'a{', desc: 'A curly braces (includes braces)' },
{ key: 'i"', desc: 'Inner double quotes' },
{ key: 'a"', desc: 'A double quotes (includes quotes)' },
{ key: "i'", desc: 'Inner single quotes' },
{ key: "a'", desc: 'A single quotes (includes quotes)' },
{ key: 'i`', desc: 'Inner backticks' },
{ key: 'it', desc: 'Inner HTML/XML tag' },
{ key: 'at', desc: 'A HTML/XML tag (includes tag)' },
],
explanations: [
{
title: '🧩 i vs a: Inside vs Around',
body: '<code class="inline">i</code> selects the inner content. <code class="inline">a</code> selects the content plus the delimiters. Example: cursor on word "hello" in <code class="inline">"hello"</code> → <code class="inline">di"</code> leaves <code class="inline">""</code>, <code class="inline">da"</code> removes the quotes too.',
},
{
title: '⚡ Combining with Operators',
body: 'Text objects shine when combined: <code class="inline">ciw</code> = change word, <code class="inline">di{</code> = delete inside braces, <code class="inline">ya"</code> = yank quoted string including quotes, <code class="inline">vi[</code> = visually select inside brackets.',
},
],
challenge: {
file: 'textobj.js',
initialText: 'function greet(name, age) {\n return "Hello, " + name + "!";\n}\n\nconst msg = greet("Alice", 30);',
instruction: 'Place cursor inside the parentheses on line 5 and type <kbd>ci(</kbd> to change the content inside the parentheses.',
hint: 'Move cursor inside the () then press ci( to delete content and enter insert mode.',
validate: (state) => state.textObjUsed === true,
},
},
{
id: 'marks',
title: 'Marks',
level: 'intermediate',
category: 'Intermediate Vim',
keys: ['ma', '`a', "'a"],
xp: 80,
desc: 'Mark positions in your file and jump back to them instantly. Bookmarks for your cursor.',
commands: [
{ key: 'ma', desc: 'Set mark a at current cursor position' },
{ key: 'mA', desc: 'Set global mark A (works across files)' },
{ key: '`a', desc: 'Jump to exact position of mark a' },
{ key: "'a", desc: "Jump to beginning of line containing mark a" },
{ key: ':marks', desc: 'List all current marks' },
{ key: ':delmarks a', desc: 'Delete mark a' },
{ key: '``', desc: 'Jump to position before last jump' },
{ key: "''", desc: 'Jump to line before last jump' },
{ key: '`[', desc: 'Jump to start of last changed/yanked text' },
{ key: '`]', desc: 'Jump to end of last changed/yanked text' },
{ key: '`<', desc: 'Jump to start of last visual selection' },
{ key: '`>', desc: 'Jump to end of last visual selection' },
],
explanations: [
{
title: '📌 Local vs Global Marks',
body: 'Lowercase marks <code class="inline">a-z</code> are local to a file. Uppercase marks <code class="inline">A-Z</code> are global and work across files — jump to mark A with <code class="inline">`A</code> even from a different buffer.',
},
],
challenge: {
file: 'marks.txt',
initialText: 'Start here (mark this line with ma)\n\n\n\n\n\nEnd here — you should jump back with `a',
instruction: 'On line 1, press <kbd>ma</kbd> to set mark a. Move to the last line with <kbd>G</kbd>. Then press <kbd>`a</kbd> to jump back.',
hint: 'ma sets mark a. `a jumps to mark a. G goes to end of file.',
validate: (state) => state.marksUsed === true,
},
},
{
id: 'macros',
title: 'Macros',
level: 'intermediate',
category: 'Intermediate Vim',
keys: ['qa', 'q', '@a', '@@'],
xp: 100,
desc: 'Record and replay sequences of commands. Automate repetitive editing tasks instantly.',
commands: [
{ key: 'qa', desc: 'Start recording macro into register a' },
{ key: 'q', desc: 'Stop recording macro' },
{ key: '@a', desc: 'Play macro from register a' },
{ key: '@@', desc: 'Replay the last played macro' },
{ key: '10@a', desc: 'Play macro a 10 times' },
{ key: ':reg a', desc: 'View contents of macro register a' },
],
explanations: [
{
title: '🎬 Recording Macros',
body: 'Think of macros as a recording of your keystrokes. <code class="inline">qa</code> starts recording into register a. Do your edits. <code class="inline">q</code> stops. <code class="inline">@a</code> replays it. <code class="inline">100@a</code> replays 100 times.',
},
{
title: '💡 Macro Best Practices',
body: 'Start macros with motion to beginning of line or a consistent position. End macros with <code class="inline">j</code> to move to the next line, so repeating applies to consecutive lines.',
},
],
challenge: {
file: 'macro.txt',
initialText: 'item one\nitem two\nitem three\nitem four\nitem five',
instruction: 'Record a macro: <kbd>qa</kbd> → <kbd>I</kbd>- <kbd>Esc</kbd> → <kbd>j</kbd> → <kbd>q</kbd>. Then play it with <kbd>4@a</kbd> to add "- " to each remaining line.',
hint: 'qa starts recording. I enters insert at line beginning. After typing, Esc, j moves down. q stops. @a plays it.',
validate: (state) => state.macroUsed === true,
},
},
{
id: 'registers',
title: 'Registers',
level: 'intermediate',
category: 'Intermediate Vim',
keys: ['"a', ':reg'],
xp: 90,
desc: 'Vim has multiple clipboards called registers. Store and retrieve text with precision.',
commands: [
{ key: ':reg', desc: 'Show all registers and their contents' },
{ key: '"ayy', desc: 'Yank current line into register a' },
{ key: '"ap', desc: 'Paste contents of register a' },
{ key: '"Ayy', desc: 'Append current line to register a (uppercase)' },
{ key: '"0p', desc: 'Paste from yank register (last yank)' },
{ key: '"+y', desc: 'Yank to system clipboard' },
{ key: '"+p', desc: 'Paste from system clipboard' },
{ key: '"_d', desc: 'Delete into black hole register (no save)' },
{ key: '"%', desc: 'Current filename register' },
{ key: '":', desc: 'Last Ex command register' },
{ key: '"/p', desc: 'Paste last search pattern' },
],
explanations: [
{
title: '📦 Types of Registers',
body: 'Unnamed <code class="inline">"</code>: default for d/y/c. Numbered <code class="inline">0-9</code>: history of deletes/yanks. Named <code class="inline">a-z</code>: manual storage. Read-only: <code class="inline">%</code> (filename), <code class="inline">.</code> (last inserted), <code class="inline">:</code> (last command). Black hole: <code class="inline">_</code> (discard).',
},
],
challenge: {
file: 'registers.txt',
initialText: 'First important line.\nSecond important line.\nThird important line.\n\n[paste here]',
instruction: 'Yank line 1 into register a with <kbd>"ayy</kbd>. Then move to the [paste here] line and paste with <kbd>"ap</kbd>.',
hint: '"ayy yanks into register a. "ap pastes from register a.',
validate: (state) => state.registerUsed === true,
},
},
{
id: 'windows-tabs',
title: 'Windows & Tabs',
level: 'intermediate',
category: 'Intermediate Vim',
keys: [':sp', ':vsp', 'Ctrl+w'],
xp: 90,
desc: 'Split your editor into multiple panes and work on multiple files simultaneously.',
commands: [
{ key: ':sp', desc: 'Split window horizontally' },
{ key: ':vsp', desc: 'Split window vertically' },
{ key: 'Ctrl+ws', desc: 'Split window horizontally' },
{ key: 'Ctrl+wv', desc: 'Split window vertically' },
{ key: 'Ctrl+ww', desc: 'Switch to next window' },
{ key: 'Ctrl+wh', desc: 'Move to left window' },
{ key: 'Ctrl+wj', desc: 'Move to window below' },
{ key: 'Ctrl+wk', desc: 'Move to window above' },
{ key: 'Ctrl+wl', desc: 'Move to right window' },
{ key: 'Ctrl+wq', desc: 'Close current window' },
{ key: 'Ctrl+w=', desc: 'Make all windows equal size' },
{ key: 'Ctrl+w+', desc: 'Increase window height' },
{ key: 'Ctrl+w-', desc: 'Decrease window height' },
{ key: ':tabnew', desc: 'Open new tab' },
{ key: 'gt', desc: 'Go to next tab' },
{ key: 'gT', desc: 'Go to previous tab' },
{ key: ':tabc', desc: 'Close current tab' },
{ key: ':tabo', desc: 'Close all other tabs' },
],
explanations: [
{
title: '🪟 Window Navigation',
body: 'All window commands start with <code class="inline">Ctrl+w</code>. After that, use <code class="inline">h j k l</code> to navigate (same direction logic), <code class="inline">s</code> for horizontal split, <code class="inline">v</code> for vertical, <code class="inline">q</code> to close.',
},
],
challenge: {
file: 'windows.txt',
initialText: 'Learn window management!\n:sp to split horizontally\n:vsp to split vertically\nCtrl+w w to switch windows',
instruction: 'This lesson is conceptual — read the commands above and use :sp to split the window.',
hint: 'Type :sp and press Enter to split horizontally.',
validate: (state) => state.windowSplitUsed === true,
},
},
{
id: 'buffers',
title: 'Buffers',
level: 'intermediate',
category: 'Intermediate Vim',
keys: [':bn', ':bp', ':ls'],
xp: 80,
desc: 'Manage multiple open files with Vim buffers — the internal representation of open files.',
commands: [
{ key: ':ls', desc: 'List all open buffers' },
{ key: ':bn', desc: 'Go to next buffer' },
{ key: ':bp', desc: 'Go to previous buffer' },
{ key: ':bd', desc: 'Delete (close) current buffer' },
{ key: ':b3', desc: 'Go to buffer 3' },
{ key: ':b filename', desc: 'Go to buffer by filename' },
{ key: ':e filename', desc: 'Edit a file (open in new buffer)' },
{ key: ':wa', desc: 'Write (save) all buffers' },
{ key: ':qa', desc: 'Quit all buffers' },
{ key: ':wqa', desc: 'Save and quit all buffers' },
],
explanations: [
{
title: '📂 Buffers vs Windows vs Tabs',
body: 'Buffer = open file in memory. Window = a view of a buffer. Tab = a collection of windows. You can have 10 buffers open, but only show 2 in windows. Multiple windows can show the same buffer.',
},
],
challenge: {
file: 'buffers.txt',
initialText: 'Buffer management is key to Vim mastery.\n:ls shows all open buffers.\n:bn and :bp navigate between them.',
instruction: 'Type <kbd>:ls</kbd> to list buffers, then type <kbd>:bn</kbd> to go to next buffer.',
hint: ':ls lists buffers. :bn goes to next buffer.',
validate: (state) => state.bufferNavUsed === true,
},
},
{
id: 'indentation',
title: 'Indentation',
level: 'intermediate',
category: 'Editing',
keys: ['>>', '<<', '>>'],
xp: 70,
desc: 'Indent and unindent lines and blocks of code with surgical precision.',
commands: [
{ key: '>>', desc: 'Indent current line right by one shiftwidth' },
{ key: '<<', desc: 'Unindent current line left by one shiftwidth' },
{ key: '5>>', desc: 'Indent 5 lines' },
{ key: '>%', desc: 'Indent to matching bracket' },
{ key: '>G', desc: 'Indent to end of file' },
{ key: '=', desc: 'Auto-indent (operator)' },
{ key: '=G', desc: 'Auto-indent to end of file' },
{ key: 'gg=G', desc: 'Auto-indent entire file' },
{ key: 'V>>', desc: 'Visual select then indent' },
{ key: '>iB', desc: 'Indent inner curly brace block' },
],
explanations: [
{
title: '⇥ Smart Indentation',
body: '<code class="inline">>></code> and <code class="inline"><<</code> indent/unindent by shiftwidth. Use <code class="inline">=</code> for auto-formatting. <code class="inline">gg=G</code> auto-indents the whole file based on your filetype settings.',
},
],
challenge: {
file: 'indent.py',
initialText: 'def hello():\nprint("world")\nfor i in range(10):\nprint(i)',
instruction: 'Move to line 2 and press <kbd>>></kbd> to indent it. Then select lines 3-4 with <kbd>Vj</kbd> and press <kbd>>></kbd> to indent them.',
hint: '>> indents the current line. In visual mode, > indents selected lines.',
validate: (state) => state.indentUsed === true,
},
},
// ── ADVANCED ──
{
id: 'operators-motions',
title: 'Operators & Motions',
level: 'advanced',
category: 'Advanced Vim',
keys: ['operator + motion'],
xp: 120,
desc: 'The Vim grammar: operators combined with motions create a powerful, composable editing language.',
commands: [
{ key: 'd', desc: 'Delete operator' },
{ key: 'c', desc: 'Change operator (delete + insert)' },
{ key: 'y', desc: 'Yank (copy) operator' },
{ key: '>', desc: 'Indent right operator' },
{ key: '<', desc: 'Indent left operator' },
{ key: '=', desc: 'Auto-indent operator' },
{ key: '~', desc: 'Toggle case operator' },
{ key: 'g~', desc: 'Toggle case (motion)' },
{ key: 'gu', desc: 'Lowercase operator' },
{ key: 'gU', desc: 'Uppercase operator' },
{ key: 'gq', desc: 'Format/wrap text operator' },
{ key: 'g@', desc: 'Apply operatorfunc' },
],
explanations: [
{
title: '🔤 The Vim Grammar',
body: 'Vim commands follow: <code class="inline">[count][operator][motion|text-object]</code>. Example: <code class="inline">3dw</code> = delete 3 words, <code class="inline">ci"</code> = change inside quotes, <code class="inline">gUiw</code> = uppercase inner word. Learn operators + motions once, combine infinitely.',
},
],
challenge: {
file: 'grammar.js',
initialText: 'const hello = "world";\nfunction greet(name) {\n return "Hi " + name;\n}\nlet x = calculateValue(a, b, c);',
instruction: 'Use <kbd>gUiw</kbd> to uppercase the word under the cursor, then <kbd>ci"</kbd> to change text inside quotes.',
hint: 'gUiw uppercases the inner word. ci" deletes inside quotes and enters insert.',
validate: (state) => state.operatorUsed === true,
},
},
{
id: 'vimrc-config',
title: 'Vim Configuration (.vimrc)',
level: 'advanced',
category: 'Configuration',
keys: [':set', 'vimrc'],
xp: 100,
desc: 'Configure Vim to your preferences with .vimrc settings, key mappings, and options.',
commands: [
{ key: ':set number', desc: 'Show line numbers' },
{ key: ':set relativenumber', desc: 'Show relative line numbers' },
{ key: ':set hlsearch', desc: 'Highlight search results' },
{ key: ':set incsearch', desc: 'Incremental search highlighting' },
{ key: ':set ignorecase', desc: 'Case-insensitive search' },
{ key: ':set smartcase', desc: 'Smart case search (with ignorecase)' },
{ key: ':set expandtab', desc: 'Use spaces instead of tabs' },
{ key: ':set tabstop=4', desc: 'Tab width = 4 spaces' },
{ key: ':set shiftwidth=4', desc: 'Indent width = 4 spaces' },
{ key: ':set autoindent', desc: 'Auto-indent new lines' },
{ key: ':set wrap', desc: 'Wrap long lines' },
{ key: ':set cursorline', desc: 'Highlight current line' },
{ key: ':set syntax on', desc: 'Enable syntax highlighting' },
{ key: ':set background=dark', desc: 'Use dark background colors' },
{ key: ':map', desc: 'Create key mapping (all modes)' },
{ key: ':nmap', desc: 'Normal mode key mapping' },
{ key: ':imap', desc: 'Insert mode key mapping' },
{ key: ':vmap', desc: 'Visual mode key mapping' },
{ key: ':colorscheme', desc: 'Set color theme' },
],
explanations: [
{
title: '⚙️ The .vimrc File',
body: 'Your <code class="inline">~/.vimrc</code> is your Vim configuration file. Commands here run every time Vim starts. You can set options, create mappings, define functions, and load plugins.',
},
{
title: '🔑 Key Mappings',
body: 'Create shortcuts with <code class="inline">nnoremap <leader>f :find</code>. Use <code class="inline">nnoremap</code> (non-recursive) instead of <code class="inline">nmap</code> to avoid mapping loops. Common leader key is <code class="inline">,</code> or <code class="inline"><Space></code>.',
},
],
challenge: {
file: '.vimrc',
initialText: '" My vimrc\nset nocompatible\nsyntax on\n\n" Add your settings below:',
instruction: 'Edit this vimrc file. Add <kbd>set number</kbd> on a new line to enable line numbers.',
hint: 'Use o to open a new line, type: set number, then press Esc',
validate: (state) => state.text.includes('set number'),
},
},
{
id: 'multiline-editing',
title: 'Multi-line Editing',
level: 'advanced',
category: 'Advanced Vim',
keys: ['Ctrl+v', 'I', 'A'],
xp: 110,
desc: 'Edit multiple lines simultaneously with Visual Block mode — add prefixes, suffixes, or make column edits.',
commands: [
{ key: 'Ctrl+v', desc: 'Enter Visual Block mode' },
{ key: 'I', desc: 'In Visual Block: insert at start of each selected line' },
{ key: 'A', desc: 'In Visual Block: append at end of each selected line' },
{ key: 'c', desc: 'In Visual Block: change selected block' },
{ key: 'd', desc: 'In Visual Block: delete selected block' },
{ key: 'r', desc: 'In Visual Block: replace all selected chars' },
{ key: 'J', desc: 'Join selected lines' },
],
explanations: [
{
title: '📊 Visual Block (Ctrl+v)',
body: 'The most powerful mode for structured data. Select a rectangle of text: useful for aligning code, adding # to comment multiple lines simultaneously, or editing CSV/TSV files.',
},
{
title: '💡 Comment Multiple Lines',
body: 'To comment 5 lines with #: <code class="inline">Ctrl+v</code> → select 5 lines with <code class="inline">5j</code> → <code class="inline">I</code> → type <code class="inline">#</code> → <code class="inline">Esc</code>. The # appears on all lines!',
},
],
challenge: {
file: 'multiline.py',
initialText: 'print("line 1")\nprint("line 2")\nprint("line 3")\nprint("line 4")\nprint("line 5")',
instruction: 'Use <kbd>Ctrl+v</kbd> to select a block of 5 lines at the beginning, then press <kbd>I</kbd> and type <kbd>#</kbd> and <kbd>Esc</kbd> to comment all 5 lines.',
hint: 'Ctrl+v enters block mode. Select down with j. I inserts at start of each line.',
validate: (state) => (state.text.match(/^#/mg) || []).length >= 3,
},
},
{
id: 'ex-commands',
title: 'Ex Commands',
level: 'advanced',
category: 'Advanced Vim',
keys: [':', 'range'],
xp: 100,
desc: 'Ex commands — the colon commands — are incredibly powerful for file operations, ranges, and automation.',
commands: [
{ key: ':w', desc: 'Write (save) file' },
{ key: ':q', desc: 'Quit' },
{ key: ':wq', desc: 'Write and quit' },
{ key: ':q!', desc: 'Force quit without saving' },
{ key: ':x', desc: 'Write and quit (like :wq but only writes if changed)' },
{ key: 'ZZ', desc: 'Save and quit (Normal mode shortcut)' },
{ key: 'ZQ', desc: 'Quit without saving (Normal mode shortcut)' },
{ key: ':e!', desc: 'Reload file from disk (discard changes)' },
{ key: ':r filename', desc: 'Read and insert file below cursor' },
{ key: ':r !cmd', desc: 'Read and insert output of shell command' },
{ key: ':!cmd', desc: 'Execute shell command' },
{ key: ':.!cmd', desc: 'Replace current line with command output' },
{ key: ':set paste', desc: 'Enable paste mode (disable auto-indent for paste)' },
{ key: ':help topic', desc: 'Open help for topic' },
{ key: ':saveas newname', desc: 'Save file with new name' },
{ key: ':g/pattern/cmd', desc: 'Run cmd on all lines matching pattern' },
{ key: ':g!/pattern/cmd', desc: 'Run cmd on all lines NOT matching pattern' },
{ key: ':v/pattern/cmd', desc: 'Same as :g! (v for inverse)' },
],
explanations: [
{
title: '🌐 Global Command :g',
body: 'The <code class="inline">:g</code> command is incredibly powerful: <code class="inline">:g/TODO/d</code> deletes all lines with TODO. <code class="inline">:g/^$/d</code> deletes all blank lines. <code class="inline">:g/pattern/normal @a</code> runs macro a on all matching lines!',
},
],
challenge: {
file: 'commands.txt',
initialText: 'Keep this.\nTODO: remove this line\nKeep this too.\nTODO: remove me also\nFinal line to keep.',
instruction: 'Type <kbd>:g/TODO/d</kbd> and press Enter to delete all lines containing "TODO".',
hint: ':g/TODO/d deletes all lines matching TODO.',
validate: (state) => !state.text.includes('TODO'),
},
},
{
id: 'advanced-search',
title: 'Advanced Search & Regex',
level: 'advanced',
category: 'Search',
keys: ['/\\v', '\\w', '\\d'],
xp: 120,
desc: 'Harness the power of regular expressions in Vim searches and substitutions.',
commands: [
{ key: '/\\v', desc: 'Very magic mode (standard regex syntax)' },
{ key: '/\\<word\\>', desc: 'Search exact whole word' },
{ key: '/\\cpattern', desc: 'Case-insensitive search' },
{ key: '/\\Cpattern', desc: 'Case-sensitive search' },
{ key: '/\\n', desc: 'Match newline in search' },
{ key: ':%s/\\n/,/', desc: 'Replace newlines with commas' },
{ key: ':%s/^/ /', desc: 'Add 2 spaces to start of every line' },
{ key: ':%s/ $//', desc: 'Remove trailing spaces' },
{ key: ':%s/\\s\\+$//', desc: 'Remove all trailing whitespace' },
{ key: '/foo\\|bar', desc: 'Search for foo OR bar' },
{ key: ':%s/\\(\\w\\+\\)/[\\1]/g', desc: 'Wrap every word in brackets (capture group)' },
],
explanations: [
{
title: '🎯 Very Magic Mode',
body: 'Prefix your pattern with <code class="inline">\\v</code> to use "very magic" mode where most special characters have regex meaning without escaping. <code class="inline">/\\v(foo|bar)\\d+</code> is much cleaner than <code class="inline">/\\(foo\\|bar\\)\\d\\+</code>.',
},
],
challenge: {
file: 'regex.txt',
initialText: ' lots of leading spaces\ntrailing spaces \n both sides \nnormal line here',
instruction: 'Type <kbd>:%s/^\\s\\+//</kbd> to remove leading whitespace from all lines.',
hint: ':%s/^\\s\\+// removes leading whitespace. ^ is start of line, \\s\\+ is one or more whitespace.',
validate: (state) => !/(^ +)/m.test(state.text),
},
},
{
id: 'jumps-changes',
title: 'Jumps & Change History',
level: 'advanced',
category: 'Advanced Vim',
keys: ['Ctrl+o', 'Ctrl+i', 'g;', 'g,'],
xp: 90,
desc: 'Navigate your jump list and change list to teleport through your editing history.',
commands: [
{ key: 'Ctrl+o', desc: 'Go to older position in jump list' },
{ key: 'Ctrl+i', desc: 'Go to newer position in jump list' },
{ key: ':jumps', desc: 'Display jump list' },
{ key: 'g;', desc: 'Go to older position in change list' },
{ key: 'g,', desc: 'Go to newer position in change list' },
{ key: ':changes', desc: 'Display change list' },
{ key: "'0", desc: 'Jump to position when last exited Vim' },
{ key: "'1-'9", desc: 'Jump to positions in exit history' },
{ key: 'Ctrl+^', desc: 'Switch to alternate (last) buffer' },
],
explanations: [
{
title: '⏱️ Jump List',
body: 'Every time you make a large jump (gg, G, /, ?, Ctrl+d, etc.) the position is saved. <code class="inline">Ctrl+o</code> goes back, <code class="inline">Ctrl+i</code> goes forward. Like browser back/forward for your cursor!',
},
],
challenge: {
file: 'jumps.txt',
initialText: Array.from({length: 15}, (_, i) => `Line ${i+1}: content here`).join('\n'),
instruction: 'Press <kbd>G</kbd> to jump to end, then <kbd>gg</kbd> to top, then <kbd>Ctrl+o</kbd> to go back to your previous position.',
hint: 'G goes to end. gg goes to top. Ctrl+o jumps back in history.',
validate: (state) => state.jumpUsed === true,
},
},
{
id: 'folding',
title: 'Folding',
level: 'advanced',
category: 'Advanced Vim',
keys: ['zf', 'zo', 'zc', 'za'],
xp: 90,
desc: 'Collapse and expand sections of code to focus on what matters.',
commands: [
{ key: 'zf{motion}', desc: 'Create fold (manual mode)' },
{ key: 'zd', desc: 'Delete fold at cursor' },
{ key: 'zo', desc: 'Open fold at cursor' },
{ key: 'zO', desc: 'Open all nested folds at cursor' },
{ key: 'zc', desc: 'Close fold at cursor' },
{ key: 'zC', desc: 'Close all nested folds at cursor' },
{ key: 'za', desc: 'Toggle fold at cursor' },
{ key: 'zA', desc: 'Toggle all folds at cursor' },
{ key: 'zm', desc: 'Fold more (decrease foldlevel)' },
{ key: 'zM', desc: 'Close all folds in file' },
{ key: 'zr', desc: 'Fold less (increase foldlevel)' },
{ key: 'zR', desc: 'Open all folds in file' },
{ key: ':set foldmethod=indent', desc: 'Fold by indentation' },
{ key: ':set foldmethod=syntax', desc: 'Fold by syntax' },
{ key: ':set foldmethod=marker', desc: 'Fold by markers ({{{, }}})' },
],
explanations: [
{
title: '📂 Fold Methods',
body: 'Vim supports 6 fold methods: manual, indent, expr, syntax, diff, marker. <code class="inline">indent</code> is great for Python. <code class="inline">syntax</code> for most languages. Set in vimrc: <code class="inline">set foldmethod=syntax</code>.',
},
],
challenge: {
file: 'fold.js',
initialText: 'function alpha() {\n const x = 1;\n return x;\n}\n\nfunction beta() {\n const y = 2;\n return y;\n}',
instruction: 'This lesson is conceptual. Practice folding commands: <kbd>zc</kbd> to close fold, <kbd>zo</kbd> to open, <kbd>za</kbd> to toggle.',
hint: 'za toggles the fold under your cursor.',
validate: (state) => state.foldUsed === true,
},
},
{
id: 'neovim-lua',
title: 'Neovim & Lua',
level: 'advanced',
category: 'Configuration',
keys: ['nvim', 'lua'],
xp: 130,
desc: 'Neovim extends Vim with Lua scripting, LSP support, and a modern plugin ecosystem.',
commands: [
{ key: ':lua print("hi")', desc: 'Run Lua code inline' },
{ key: ':luafile file.lua', desc: 'Run a Lua file' },
{ key: 'require("module")', desc: 'Load a Lua module in config' },
{ key: 'vim.opt.number = true', desc: 'Set number option in Lua' },