vimniscient 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. {vimniscient-0.3.0 → vimniscient-0.3.1}/PKG-INFO +2 -1
  2. {vimniscient-0.3.0 → vimniscient-0.3.1}/README.md +1 -0
  3. {vimniscient-0.3.0 → vimniscient-0.3.1}/pyproject.toml +1 -1
  4. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/app.py +8 -18
  5. vimniscient-0.3.1/src/vimniscient/card_session.py +81 -0
  6. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/diagnose.py +11 -0
  7. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/endless.py +6 -0
  8. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w1d2-pairs.yaml +6 -0
  9. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w1d3-sentences.yaml +51 -0
  10. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w1d4-find.yaml +31 -0
  11. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w1d5-lines.yaml +23 -0
  12. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w1d6-counts.yaml +40 -0
  13. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w1d7-review.yaml +29 -0
  14. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w2d1-dot.yaml +20 -0
  15. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w2d2-search.yaml +21 -0
  16. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w2d3-cgn.yaml +36 -0
  17. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w2d4-case-numbers.yaml +38 -0
  18. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w2d5-insert.yaml +42 -0
  19. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w2d6-visual.yaml +13 -0
  20. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w2d7-review.yaml +37 -0
  21. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w3d1-macros.yaml +37 -0
  22. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w3d2-macros-more.yaml +35 -0
  23. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w3d3-registers.yaml +49 -0
  24. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w3d4-marks.yaml +35 -0
  25. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w3d5-visual-block.yaml +26 -0
  26. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w3d6-substitute.yaml +48 -0
  27. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w3d7-review.yaml +45 -0
  28. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w4d1-ranges.yaml +12 -0
  29. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w4d2-sort-filter.yaml +36 -0
  30. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w4d3-regex.yaml +5 -0
  31. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w4d4-norm.yaml +16 -0
  32. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w4d5-motions.yaml +25 -0
  33. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w4d6-indent.yaml +29 -0
  34. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w4d7-review.yaml +39 -0
  35. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w5d1-arguments.yaml +27 -0
  36. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w5d2-any-quote-bracket.yaml +26 -0
  37. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w5d3-functions.yaml +40 -0
  38. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w5d4-small-objects.yaml +41 -0
  39. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w5d5-comments.yaml +30 -0
  40. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/missions.py +3 -0
  41. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/placement.py +9 -0
  42. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/quick.py +4 -0
  43. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/today.py +6 -0
  44. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/transfer.py +8 -0
  45. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/cards.py +24 -59
  46. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/lesson.py +2 -7
  47. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/main.py +1 -4
  48. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_answers.py +8 -7
  49. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_app.py +4 -4
  50. vimniscient-0.3.1/tests/golden/answers.json +329 -0
  51. vimniscient-0.3.1/tests/golden/cards.json +2417 -0
  52. vimniscient-0.3.1/tests/golden/diagnose.json +108 -0
  53. vimniscient-0.3.1/tests/golden/keys.json +2221 -0
  54. vimniscient-0.3.1/tests/golden/lessons.json +12762 -0
  55. vimniscient-0.3.1/tests/golden/placement.json +232 -0
  56. vimniscient-0.3.1/tests/golden/schema.sql +57 -0
  57. vimniscient-0.3.1/tests/golden/srs.json +231 -0
  58. vimniscient-0.3.1/tests/golden/today.json +564 -0
  59. vimniscient-0.3.1/tests/make_golden.py +145 -0
  60. {vimniscient-0.3.0 → vimniscient-0.3.1}/.gitignore +0 -0
  61. {vimniscient-0.3.0 → vimniscient-0.3.1}/LICENSE +0 -0
  62. {vimniscient-0.3.0 → vimniscient-0.3.1}/extras/nvim/vimniscient.lua +0 -0
  63. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/__init__.py +0 -0
  64. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/__main__.py +0 -0
  65. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/answer.py +0 -0
  66. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/cards/foot.yaml +0 -0
  67. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/cards/lazyvim.yaml +0 -0
  68. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/cards/shell.yaml +0 -0
  69. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/cards/vim.yaml +0 -0
  70. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/cards/zathura.yaml +0 -0
  71. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/cards.py +0 -0
  72. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/cli.py +0 -0
  73. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/doctor.py +0 -0
  74. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/drills/omarchy.yaml +0 -0
  75. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/drills.py +0 -0
  76. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/eval.yaml +0 -0
  77. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/evaluation.py +0 -0
  78. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/generate.py +0 -0
  79. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/habits.py +0 -0
  80. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/keys.py +0 -0
  81. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/sources.md +0 -0
  82. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w1d1-words.yaml +0 -0
  83. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w5d6-review.yaml +0 -0
  84. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w6d1-code.yaml +0 -0
  85. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w6d2-notes.yaml +0 -0
  86. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons/w6d3-data.yaml +0 -0
  87. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/lessons.py +0 -0
  88. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/missions.yaml +0 -0
  89. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/mnemonics.py +0 -0
  90. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/mnemonics.yaml +0 -0
  91. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/nvim/keymaps.lua +0 -0
  92. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/nvim/mission.lua +0 -0
  93. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/nvim/runner.lua +0 -0
  94. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/progress.py +0 -0
  95. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/reminders.py +0 -0
  96. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/session.py +0 -0
  97. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/settings.py +0 -0
  98. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/srs.py +0 -0
  99. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/transfer.yaml +0 -0
  100. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/__init__.py +0 -0
  101. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/builtin_lessons.py +0 -0
  102. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/common.py +0 -0
  103. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/drills.py +0 -0
  104. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/quick.py +0 -0
  105. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/ui/stats.py +0 -0
  106. {vimniscient-0.3.0 → vimniscient-0.3.1}/src/vimniscient/vimgolf.py +0 -0
  107. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_diagnose.py +0 -0
  108. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_drills.py +0 -0
  109. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_generate.py +0 -0
  110. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_lessons.lua +0 -0
  111. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_lessons.py +0 -0
  112. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_missions.py +0 -0
  113. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_placement.py +0 -0
  114. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_quick.py +0 -0
  115. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_timing.py +0 -0
  116. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_today.py +0 -0
  117. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/check_transfer.py +0 -0
  118. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/live_drills.py +0 -0
  119. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/live_endless.py +0 -0
  120. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/live_lessons.py +0 -0
  121. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/live_mission.py +0 -0
  122. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/live_retry.py +0 -0
  123. {vimniscient-0.3.0 → vimniscient-0.3.1}/tests/live_review.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vimniscient
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Get fluent in Vim in fifteen minutes a day, in your own Neovim.
5
5
  Project-URL: Homepage, https://vimniscient.com
6
6
  Project-URL: Source, https://github.com/dialectmaterial/Vimniscient
@@ -317,6 +317,7 @@ uv run python tests/check_transfer.py # Choose: every way solves it in
317
317
  uv run python tests/check_quick.py # quick practice: the right tasks for the time, nothing graded
318
318
  uv run python tests/check_missions.py # missions: every way does the whole job on real files
319
319
  uv run python tests/check_placement.py # placement, the evaluation and doctor
320
+ uv run python tests/make_golden.py --check # the reference files for the Rust port are up to date
320
321
  uv run python tests/live_mission.py # a mission in your real Neovim, in a hidden tmux session
321
322
  uv run python tests/check_today.py # the daily budget, SRS scheduling, card introductions, hardtime
322
323
  uv run python tests/check_app.py # every screen, driven headless
@@ -294,6 +294,7 @@ uv run python tests/check_transfer.py # Choose: every way solves it in
294
294
  uv run python tests/check_quick.py # quick practice: the right tasks for the time, nothing graded
295
295
  uv run python tests/check_missions.py # missions: every way does the whole job on real files
296
296
  uv run python tests/check_placement.py # placement, the evaluation and doctor
297
+ uv run python tests/make_golden.py --check # the reference files for the Rust port are up to date
297
298
  uv run python tests/live_mission.py # a mission in your real Neovim, in a hidden tmux session
298
299
  uv run python tests/check_today.py # the daily budget, SRS scheduling, card introductions, hardtime
299
300
  uv run python tests/check_app.py # every screen, driven headless
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vimniscient"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "Get fluent in Vim in fifteen minutes a day, in your own Neovim."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-or-later"
@@ -21,7 +21,7 @@ from vimniscient.progress import Progress
21
21
  from vimniscient.session import COLORS, TaskResult, mouse_wheel, run_endless, run_lesson, run_mission
22
22
  from vimniscient.ui.builtin_lessons import endless_lesson, mission_lesson, placement_lesson, quick_lesson, review_lesson, transfer_lesson, vimgolf_lesson
23
23
  from vimniscient.ui.cards import CardScreen
24
- from vimniscient.ui.common import CATPPUCCIN, DEFAULT_THEME, omarchy_theme, plural
24
+ from vimniscient.ui.common import CATPPUCCIN, DEFAULT_THEME, omarchy_theme
25
25
  from vimniscient.ui.drills import DrillScreen
26
26
  from vimniscient.ui.lesson import LessonScreen, ResultsScreen
27
27
  from vimniscient.ui.main import MainScreen
@@ -260,12 +260,10 @@ class VimniscientApp(App):
260
260
  search.update(results)
261
261
  search.seen |= {r.task.item_id for r in results}
262
262
  if not search.finished:
263
- return "That round is done. The next one follows from how it went."
263
+ return placement.NEXT_ROUND
264
264
  known = search.known()
265
265
  self.progress.mark_known(known, search.seen)
266
- if not known:
267
- return "No lessons skipped. Start from Week 1 · Day 1."
268
- return "Known already: " + ", ".join(f"{p.label} ({p.title})" for p in known)
266
+ return placement.note(known)
269
267
 
270
268
  def after() -> None:
271
269
  self.placement_round(search, done) if not search.finished else done()
@@ -291,9 +289,8 @@ class VimniscientApp(App):
291
289
  if skill == "mission":
292
290
  self.start_mission(mission, done)
293
291
  return
294
- count, seed = quick.how_many(minutes, self.progress.typical_task_seconds()), datetime.now().isoformat()
295
- tasks = (quick.navigation(count, self.lessons[0].tasks[0], seed) if skill == quick.NAVIGATION
296
- else quick.tasks(pools[skill], count, seed))
292
+ count = quick.how_many(minutes, self.progress.typical_task_seconds())
293
+ tasks = quick.build(skill, count, pools, self.lessons[0].tasks[0], datetime.now().isoformat())
297
294
 
298
295
  def on_results(results: list[TaskResult]) -> str:
299
296
  self.progress.record_ungraded(results, "quick")
@@ -308,8 +305,7 @@ class VimniscientApp(App):
308
305
  result = run_mission(mission)
309
306
  mouse_wheel(False)
310
307
  self.progress.record_ungraded([result], "mission")
311
- ways = "\n".join(mission.ways)
312
- note = Content(f"{'Another way' if result.status == 'solved' else 'Ways to do it'}\n{ways}")
308
+ note = Content(mission.note(result.status == "solved"))
313
309
 
314
310
  def after(again: bool) -> None:
315
311
  self.start_mission(mission, done) if again else done()
@@ -322,10 +318,7 @@ class VimniscientApp(App):
322
318
  def start_transfer(self, picks: list[transfer.Challenge], done: Callable[[], None]) -> None:
323
319
  def on_results(results: list[TaskResult]) -> Content:
324
320
  self.progress.record_ungraded(results, "transfer")
325
- by_goal = {c.task.goal: c for c in picks}
326
- lines = [f"{r.task.goal} {' or '.join(ways)}" for r in results
327
- if (ways := transfer.another_way(by_goal[r.task.goal], r.keys, r.status == "solved"))]
328
- return Content("Another way\n" + "\n".join(lines)) if lines else Content("")
321
+ return Content(transfer.note(picks, results))
329
322
 
330
323
  self.start_lesson(transfer_lesson([c.task for c in picks]), on_results, done)
331
324
 
@@ -339,10 +332,7 @@ class VimniscientApp(App):
339
332
  mouse_wheel(False)
340
333
  run = endless.summary(results, start)
341
334
  self.progress.record_endless(results, run)
342
- note = (f"Level {run['start']} → {run['level']} · {plural(run['missed'], 'miss', 'misses')}, "
343
- f"{run['fixed']} recalled afterwards")
344
- if run["level"] > best and run["climbed"]:
345
- note += " · a new best!"
335
+ note = endless.note(run, best)
346
336
 
347
337
  def after(again: bool) -> None:
348
338
  if again:
@@ -0,0 +1,81 @@
1
+ """One run through a queue of cards: what's asked next and how each answer grades.
2
+
3
+ A card you've never answered from memory is introduced: its answer is shown and
4
+ you type it once. A miss comes back at the end, but only the first try counts.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+
9
+ from vimniscient import srs
10
+ from vimniscient.answer import is_correct
11
+ from vimniscient.cards import Card
12
+ from vimniscient.progress import Progress
13
+
14
+
15
+ @dataclass
16
+ class CardSession:
17
+ progress: Progress
18
+ queue: list[Card]
19
+ total: int = 0
20
+ retried: set[str] = field(default_factory=set)
21
+ pending: tuple[Card, bool, str] | None = None
22
+ intro: bool = False # the card on screen is new to you
23
+ correct: int = 0
24
+ answered: int = 0
25
+
26
+ def __post_init__(self) -> None:
27
+ self.queue = list(self.queue)
28
+ self.total = len(self.queue)
29
+
30
+ @property
31
+ def done(self) -> int:
32
+ return self.total - len([c for c in self.queue if c.id not in self.retried])
33
+
34
+ @property
35
+ def card(self) -> Card | None:
36
+ return self.queue[0] if self.queue else None
37
+
38
+ def begin(self) -> Card | None:
39
+ """The next card, and whether to introduce it."""
40
+ card = self.card
41
+ if card:
42
+ item = self.progress.item(card.id)
43
+ # never answered from memory, and not introduced since the last miss
44
+ self.intro = card.id not in self.retried and not self.progress.ever_right(card.id) and (not item or item.reps == 0)
45
+ return card
46
+
47
+ def submit(self, answer: str) -> bool:
48
+ """False when an introduced card was copied wrong, so it's asked again."""
49
+ card = self.queue[0]
50
+ correct = is_correct(answer, card.answers, card.style, card.case_sensitive)
51
+ if self.intro and not correct:
52
+ return False
53
+ self.pending = (card, correct, answer)
54
+ return True
55
+
56
+ def override(self) -> None:
57
+ """"I was right": a miss the checker was too strict about."""
58
+ if self.pending and not self.pending[1]:
59
+ self.pending = (self.pending[0], True, self.pending[2])
60
+
61
+ def settle(self) -> None:
62
+ """Grade the answered card (only the first try of a session counts)."""
63
+ if not self.pending:
64
+ return
65
+ card, correct, answer = self.pending
66
+ self.pending = None
67
+ self.queue.pop(0)
68
+ if self.intro: # typed with the answer shown: scheduled for tomorrow, not scored
69
+ self.progress.grade(card.id, "card", srs.GOOD)
70
+ self.progress.log_card(card, True, answer, intro=True) # still practice, for the streak
71
+ return
72
+ if card.id in self.retried:
73
+ return
74
+ self.answered += 1
75
+ self.correct += correct
76
+ self.progress.log_card(card, correct, answer)
77
+ known = self.progress.item(card.id) is not None
78
+ self.progress.grade(card.id, "card", (srs.GOOD if known else srs.EASY) if correct else srs.AGAIN)
79
+ if not correct:
80
+ self.retried.add(card.id)
81
+ self.queue.append(card)
@@ -84,6 +84,17 @@ def normal_keys(keys: str) -> list[list[str]]:
84
84
  return [*runs, run] if run else runs
85
85
 
86
86
 
87
+ def results_summary(results: list) -> str:
88
+ """One line on how a run went: solved, beat the reference, the median time."""
89
+ recall = [r for r in results if r.task.phase != "learn"]
90
+ solved = [r for r in recall if r.status == "solved"]
91
+ beat = sum(r.count < r.task.ref for r in solved)
92
+ times = sorted(r.seconds for r in solved if r.seconds is not None)
93
+ verb = "recalled" if len(recall) < len(results) else "solved"
94
+ return (f"{len(solved)} of {len(recall)} {verb}" + (f" · {beat} beat the reference" if beat else "")
95
+ + (f" · {times[len(times) // 2]:.1f}s a task" if times else ""))
96
+
97
+
87
98
  def _longest_run(runs: list[list[str]], wanted: set[str]) -> tuple[str, int]:
88
99
  best, length = "", 0
89
100
  for tokens in runs:
@@ -49,6 +49,12 @@ def ladder(lessons: list[Lesson], start: int, length: int = RUNGS, seed: object
49
49
  return rungs
50
50
 
51
51
 
52
+ def note(run: dict[str, int], best: int) -> str:
53
+ misses = f"{run['missed']} miss" if run["missed"] == 1 else f"{run['missed']} misses"
54
+ text = f"Level {run['start']} → {run['level']} · {misses}, {run['fixed']} recalled afterwards"
55
+ return text + " · a new best!" if run["level"] > best and run["climbed"] else text
56
+
57
+
52
58
  def summary(results: list, start: int) -> dict[str, int]:
53
59
  climbed = sum(r.task.phase == "climb" and r.status == "solved" for r in results)
54
60
  return {
@@ -105,3 +105,9 @@ tasks:
105
105
  target: |-
106
106
  <b>the tags stay</b>
107
107
  solution: 'citthe tags stay<Esc>'
108
+ - goal: 'Change the text inside the tag to "it is inside".'
109
+ start: |-
110
+ <i>only the ▮text goes</i>
111
+ target: |-
112
+ <i>it is inside</i>
113
+ solution: 'citit is inside<Esc>'
@@ -35,6 +35,19 @@ tasks:
35
35
  a is around: the paragraph and the blank line after it.
36
36
 
37
37
  One gap stays.
38
+ - goal: 'Delete the middle paragraph.'
39
+ start: |-
40
+ a paragraph runs to the blank line.
41
+
42
+ This block goes▮.
43
+ All of it.
44
+
45
+ It leaves no double gap behind.
46
+ target: |-
47
+ a paragraph runs to the blank line.
48
+
49
+ It leaves no double gap behind.
50
+ solution: 'dap'
38
51
  - goal: 'Duplicate the first paragraph.'
39
52
  start: |-
40
53
  ▮yap yanks a paragraph; P puts it above.
@@ -59,6 +72,18 @@ tasks:
59
72
  P puts above, p puts below.
60
73
 
61
74
  Done.
75
+ - goal: 'Duplicate the first paragraph.'
76
+ start: |-
77
+ ▮y yanks, a is around, p is paragraph.
78
+
79
+ Done.
80
+ target: |-
81
+ y yanks, a is around, p is paragraph.
82
+
83
+ y yanks, a is around, p is paragraph.
84
+
85
+ Done.
86
+ solution: 'yapP'
62
87
  - goal: 'Fix the wrong sentence: it should say "A sentence ends at a full stop."'
63
88
  start: |-
64
89
  s is for sentence. A sentence ends at a ▮comma. cis changes just one.
@@ -73,6 +98,12 @@ tasks:
73
98
  target: |-
74
99
  cis keeps the spaces. The i means inner. das takes one along.
75
100
  solution: 'cisThe i means inner.<Esc>'
101
+ - goal: 'Fix the wrong sentence: it should say "A sentence ends at a full stop."'
102
+ start: |-
103
+ c changes. A sentence ends at a ▮comma. The next one stays.
104
+ target: |-
105
+ c changes. A sentence ends at a full stop. The next one stays.
106
+ solution: 'cisA sentence ends at a full stop.<Esc>'
76
107
  - goal: 'Delete the middle sentence.'
77
108
  start: |-
78
109
  das deletes a sentence. This one ▮is wrong. Its space goes too.
@@ -85,6 +116,12 @@ tasks:
85
116
  a is around: a sentence and its space. This sentence ▮is spare. The next one moves up.
86
117
  target: |-
87
118
  a is around: a sentence and its space. The next one moves up.
119
+ - goal: 'Delete the middle sentence.'
120
+ start: |-
121
+ d deletes. This sent▮ence goes. a takes the space.
122
+ target: |-
123
+ d deletes. a takes the space.
124
+ solution: 'das'
88
125
  - goal: 'Indent the three lines.'
89
126
  start: |-
90
127
  What ip covers:
@@ -114,3 +151,17 @@ tasks:
114
151
  one
115
152
  two
116
153
  three
154
+ - goal: 'Indent the three lines.'
155
+ start: |-
156
+ i is inner, p is paragraph:
157
+
158
+ ▮the block
159
+ the block
160
+ then > indents
161
+ target: |-
162
+ i is inner, p is paragraph:
163
+
164
+ the block
165
+ the block
166
+ then > indents
167
+ solution: '>ip'
@@ -39,6 +39,12 @@ tasks:
39
39
  target: |-
40
40
  let f = "find"; let t = "till";
41
41
  solution: 'ct;"find"<Esc>'
42
+ - goal: 'Change the value to "till" (keep the semicolon).'
43
+ start: |-
44
+ let dt = ▮"lands"; let df = "find";
45
+ target: |-
46
+ let dt = "till"; let df = "find";
47
+ solution: 'ct;"till"<Esc>'
42
48
  - goal: 'Keep only the folder path: delete the file name.'
43
49
  start: |-
44
50
  lessons/week-1/till/backwards.ya▮ml
@@ -51,6 +57,12 @@ tasks:
51
57
  vim/motions/capital-T/goes-back.tx▮t
52
58
  target: |-
53
59
  vim/motions/capital-T/
60
+ - goal: 'Keep only the folder path: delete the file name.'
61
+ start: |-
62
+ keys/capital/T/looks-back.m▮d
63
+ target: |-
64
+ keys/capital/T/
65
+ solution: 'T/D'
54
66
  - goal: 'Turn the third comma into a semicolon.'
55
67
  start: |-
56
68
  ▮count, then f, then the comma, done
@@ -63,6 +75,12 @@ tasks:
63
75
  ▮f, t, F, T
64
76
  target: |-
65
77
  f, t, F; T
78
+ - goal: 'Turn the third comma into a semicolon.'
79
+ start: |-
80
+ ▮one, two, three, then r
81
+ target: |-
82
+ one, two, three; then r
83
+ solution: '3f,r;'
66
84
  - goal: 'Replace the whole before(char) call with on_it.'
67
85
  ft: python
68
86
  start: |-
@@ -78,6 +96,13 @@ tasks:
78
96
  target: |-
79
97
  t_stops = till # one short of it
80
98
  solution: 'ct till<Esc>'
99
+ - goal: 'Replace the whole past(char) call with till_it.'
100
+ ft: python
101
+ start: |-
102
+ ct_stops = ▮past(char) # ct stops at the space
103
+ target: |-
104
+ ct_stops = till_it # ct stops at the space
105
+ solution: 'ct till_it<Esc>'
81
106
  - goal: 'Delete the part in brackets: from the cursor up to and including the ")".'
82
107
  start: |-
83
108
  f lands on▮ (not before) the letter.
@@ -90,3 +115,9 @@ tasks:
90
115
  F finds backwards▮ (to the left) on the line.
91
116
  target: |-
92
117
  F finds backwards on the line.
118
+ - goal: 'Delete the part in brackets: from the cursor up to and including the ")".'
119
+ start: |-
120
+ df deletes▮ (through) the letter.
121
+ target: |-
122
+ df deletes the letter.
123
+ solution: 'df)'
@@ -41,6 +41,12 @@ tasks:
41
41
  target: |-
42
42
  C is a big c: it changes the rest of the line
43
43
  solution: 'Cthe rest of the line<Esc>'
44
+ - goal: 'Change the rest of the line to "the end of the line".'
45
+ start: |-
46
+ C goes big: ▮a little
47
+ target: |-
48
+ C goes big: the end of the line
49
+ solution: 'Cthe end of the line<Esc>'
44
50
  - goal: 'Replace the middle line with "cc changes a line".'
45
51
  start: |-
46
52
  c changes
@@ -63,6 +69,16 @@ tasks:
63
69
  cc changes a line
64
70
  yy yanks a line
65
71
  solution: 'cccc changes a line<Esc>'
72
+ - goal: 'Replace the middle line with "cc changes a line".'
73
+ start: |-
74
+ p puts a line
75
+ ▮ww changes a line
76
+ o opens a line
77
+ target: |-
78
+ p puts a line
79
+ cc changes a line
80
+ o opens a line
81
+ solution: 'cccc changes a line<Esc>'
66
82
  - goal: 'Join the two lines.'
67
83
  generate: J-split
68
84
  start: |-
@@ -134,3 +150,10 @@ tasks:
134
150
  start = ▮"I goes to the first letter"
135
151
  target: |-
136
152
  # start = "I goes to the first letter"
153
+ - goal: 'Comment the line out with "# ".'
154
+ ft: python
155
+ start: |-
156
+ big_i = ▮"capital I starts the line"
157
+ target: |-
158
+ # big_i = "capital I starts the line"
159
+ solution: 'I# <Esc>'
@@ -63,6 +63,12 @@ tasks:
63
63
  target: |-
64
64
  c2w: change two words.
65
65
  solution: 'c2wtwo words<Esc>'
66
+ - goal: 'Change "three words" to "two words".'
67
+ start: |-
68
+ 2 on cw changes ▮three words.
69
+ target: |-
70
+ 2 on cw changes two words.
71
+ solution: 'c2wtwo words<Esc>'
66
72
  - goal: 'Fix the number: 5, not 500000.'
67
73
  generate: count-x
68
74
  start: |-
@@ -90,6 +96,11 @@ tasks:
90
96
  ▮c changes, d deletes, y yanks, z naps
91
97
  target: |-
92
98
  c changes, d deletes, y yanks
99
+ - goal: 'Delete from the third comma to the end (z is not an operator).'
100
+ start: |-
101
+ ▮p puts, x cuts, u undoes, z naps
102
+ target: |-
103
+ p puts, x cuts, u undoes
93
104
  - goal: 'Join the three lines into one.'
94
105
  start: |-
95
106
  ▮3J
@@ -146,6 +157,23 @@ tasks:
146
157
  four
147
158
  six
148
159
  solution: '5jdd'
160
+ - goal: 'Delete the line five lines below the cursor.'
161
+ start: |-
162
+ ▮the count is relative:
163
+ one
164
+ two
165
+ three
166
+ four
167
+ this line goes
168
+ six
169
+ target: |-
170
+ the count is relative:
171
+ one
172
+ two
173
+ three
174
+ four
175
+ six
176
+ solution: '5jdd'
149
177
 
150
178
  - goal: 'Fix "Teh" three lines up.'
151
179
  start: |-
@@ -173,3 +201,15 @@ tasks:
173
201
  so 3k goes
174
202
  three lines up.
175
203
  solution: '3kciwRelative<Esc>'
204
+ - goal: 'Fix "Cuont" three lines up.'
205
+ start: |-
206
+ Cuont first
207
+ then k,
208
+ so 3k goes
209
+ ▮three lines up.
210
+ target: |-
211
+ Count first
212
+ then k,
213
+ so 3k goes
214
+ three lines up.
215
+ solution: '3kciwCount<Esc>'
@@ -58,6 +58,13 @@ tasks:
58
58
  target: |-
59
59
  ci_paren(inside)
60
60
  solution: 'ci(inside<Esc>'
61
+ - goal: 'Replace all three arguments with new.'
62
+ ft: python
63
+ start: |-
64
+ args(▮old, old, old)
65
+ target: |-
66
+ args(new)
67
+ solution: 'ci(new<Esc>'
61
68
  - goal: 'Delete the wrong sentence.'
62
69
  start: |-
63
70
  d deletes. ▮y means yell. c changes.
@@ -96,6 +103,17 @@ tasks:
96
103
  target: |-
97
104
  import yank
98
105
  import put
106
+ - goal: 'Delete the cut_one and cut_two imports.'
107
+ ft: python
108
+ start: |-
109
+ import two
110
+ ▮import cut_one
111
+ import cut_two
112
+ import dd
113
+ target: |-
114
+ import two
115
+ import dd
116
+ solution: '2dd'
99
117
  - goal: 'Change the subdomain to www.'
100
118
  start: |-
101
119
  url = 'https://▮old.vimhelp.org'
@@ -133,3 +151,14 @@ tasks:
133
151
  def put():
134
152
  text = yanked
135
153
  return text
154
+ - goal: 'Indent the body of the function.'
155
+ ft: python
156
+ start: |-
157
+ def indent():
158
+ ▮push = ">"
159
+ return push
160
+ target: |-
161
+ def indent():
162
+ push = ">"
163
+ return push
164
+ solution: '>j'
@@ -23,6 +23,12 @@ tasks:
23
23
  target: |-
24
24
  dot = again + again
25
25
  solution: 'ciwagain<Esc>ww.'
26
+ - goal: 'Change both "change" to "repeat".'
27
+ start: |-
28
+ keys = ▮change + change
29
+ target: |-
30
+ keys = repeat + repeat
31
+ solution: 'ciwrepeat<Esc>ww.'
26
32
  - goal: 'Add a semicolon to every line.'
27
33
  generate: dot-append
28
34
  start: |-
@@ -60,6 +66,14 @@ tasks:
60
66
  target: |-
61
67
  ␣ repeat
62
68
  the change
69
+ - goal: 'Indent both lines.'
70
+ start: |-
71
+ ▮> then j
72
+ then dot
73
+ target: |2-
74
+ > then j
75
+ then dot
76
+ solution: '>>j.'
63
77
  - goal: 'Delete all three "again".'
64
78
  start: |-
65
79
  The dot ▮again again again repeats the last change.
@@ -73,6 +87,12 @@ tasks:
73
87
  The . key ▮dot dot dot repeats your last change.
74
88
  target: |-
75
89
  The . key repeats your last change.
90
+ - goal: 'Delete all three "repeat".'
91
+ start: |-
92
+ The dot ▮repeat repeat repeat repeats the last change.
93
+ target: |-
94
+ The dot repeats the last change.
95
+ solution: 'dw..'
76
96
  - goal: 'Turn the lines into a list with "- ".'
77
97
  generate: dot-prefix
78
98
  start: |-
@@ -51,6 +51,12 @@ tasks:
51
51
  target: |-
52
52
  Slash leans forward
53
53
  solution: 'd/S<CR>'
54
+ - goal: 'Delete the hedging before "So".'
55
+ start: |-
56
+ ▮um, well, so, So d/ goes till it
57
+ target: |-
58
+ So d/ goes till it
59
+ solution: 'd/S<CR>'
54
60
  - goal: 'Rename the second "starred" to found.'
55
61
  ft: python
56
62
  start: |-
@@ -70,6 +76,15 @@ tasks:
70
76
  star = "*"
71
77
  found = match
72
78
  solution: '*ciwmatch<Esc>'
79
+ - goal: 'Rename the second "marked" to found.'
80
+ ft: python
81
+ start: |-
82
+ ▮marked = "this word"
83
+ then = marked
84
+ target: |-
85
+ marked = "this word"
86
+ then = found
87
+ solution: '*ciwfound<Esc>'
73
88
  - goal: 'Delete the third "next".'
74
89
  start: |-
75
90
  n means ▮next, and N means the next next one back.
@@ -102,3 +117,9 @@ tasks:
102
117
  target: |-
103
118
  ? asks backward; / leans forward; ? looks backward from the end.
104
119
  solution: '?fo<CR>cwbackward<Esc>'
120
+ - goal: 'Change the last "forward" to "backward", searching backwards.'
121
+ start: |-
122
+ the slash goes forward; ? asks forward, from the en▮d.
123
+ target: |-
124
+ the slash goes forward; ? asks backward, from the end.
125
+ solution: '?fo<CR>cwbackward<Esc>'
@@ -33,6 +33,16 @@ tasks:
33
33
  c(match)
34
34
  dot(match)
35
35
  solution: '*cgnmatch<Esc>..'
36
+ - goal: 'Rename every x to match.'
37
+ start: |-
38
+ ▮x = star()
39
+ next(x)
40
+ dot(x)
41
+ target: |-
42
+ match = star()
43
+ next(match)
44
+ dot(match)
45
+ solution: '*cgnmatch<Esc>..'
36
46
  - goal: 'Change the first and the last "the" to "a", keep the middle one.'
37
47
  start: |-
38
48
  ▮the change, the skip and the repeat
@@ -45,6 +55,12 @@ tasks:
45
55
  ▮the star, the next and the dot
46
56
  target: |-
47
57
  a star, the next and a dot
58
+ - goal: 'Change the first and the last "the" to "a", keep the middle one.'
59
+ start: |-
60
+ ▮the find, the pass and the dot
61
+ target: |-
62
+ a find, the pass and a dot
63
+ solution: '*Ncgna<Esc>nn.'
48
64
  - goal: 'Replace "the last" with "the next" on both lines.'
49
65
  start: |-
50
66
  ▮gn goes to the last match.
@@ -63,6 +79,14 @@ tasks:
63
79
  cgn changes the next match.
64
80
  The dot changes the next match.
65
81
  solution: '/one match<CR>cgnthe next match<Esc>.'
82
+ - goal: 'Replace "one match" with "the next match" on both lines.'
83
+ start: |-
84
+ ▮gn finds one match.
85
+ Dot repeats on one match.
86
+ target: |-
87
+ gn finds the next match.
88
+ Dot repeats on the next match.
89
+ solution: '/one match<CR>cgnthe next match<Esc>.'
66
90
  - goal: 'Change all four match to dot.'
67
91
  start: |-
68
92
  cgn ▮match; cgn match; cgn match; cgn match
@@ -77,6 +101,12 @@ tasks:
77
101
  target: |-
78
102
  say again; say again; say again; say again
79
103
  solution: '*cgnagain<Esc>...'
104
+ - goal: 'Change all four it to again.'
105
+ start: |-
106
+ do i▮t; do it; do it; do it
107
+ target: |-
108
+ do again; do again; do again; do again
109
+ solution: '*cgnagain<Esc>...'
80
110
  - goal: 'Delete every "um, ".'
81
111
  start: |-
82
112
  dgn ▮um, deletes um, the next match um, each time.
@@ -91,3 +121,9 @@ tasks:
91
121
  target: |-
92
122
  dgn deletes every match in turn.
93
123
  solution: '/so, <CR>dgn..'
124
+ - goal: 'Delete every "er, ".'
125
+ start: |-
126
+ dgn ▮er, cuts er, the next match er, each time.
127
+ target: |-
128
+ dgn cuts the next match each time.
129
+ solution: '/er, <CR>dgn..'