vimniscient 0.2.0__tar.gz → 0.2.2__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.
- {vimniscient-0.2.0 → vimniscient-0.2.2}/PKG-INFO +3 -1
- {vimniscient-0.2.0 → vimniscient-0.2.2}/README.md +2 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/pyproject.toml +1 -1
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/answer.py +2 -2
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/app.py +23 -5
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/cards/lazyvim.yaml +1 -1
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/cards/shell.yaml +3 -3
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/cards/vim.yaml +2 -2
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/cards.py +1 -1
- vimniscient-0.2.2/src/vimniscient/diagnose.py +113 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/endless.py +1 -1
- vimniscient-0.2.2/src/vimniscient/generate.py +215 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/habits.py +2 -1
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/sources.md +3 -3
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w1d1-words.yaml +1 -39
- vimniscient-0.2.2/src/vimniscient/lessons/w1d2-pairs.yaml +107 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w1d3-sentences.yaml +116 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w1d4-find.yaml +92 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w1d5-lines.yaml +136 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w1d6-counts.yaml +175 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w1d7-review.yaml +16 -103
- vimniscient-0.2.2/src/vimniscient/lessons/w2d1-dot.yaml +94 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w2d2-search.yaml +1 -91
- vimniscient-0.2.2/src/vimniscient/lessons/w2d3-cgn.yaml +93 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w2d4-case-numbers.yaml +101 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w2d5-insert.yaml +107 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w2d6-visual.yaml +103 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w2d7-review.yaml +99 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w3d1-macros.yaml +19 -125
- vimniscient-0.2.2/src/vimniscient/lessons/w3d2-macros-more.yaml +97 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w3d3-registers.yaml +4 -104
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w3d4-marks.yaml +6 -89
- vimniscient-0.2.2/src/vimniscient/lessons/w3d5-visual-block.yaml +114 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w3d6-substitute.yaml +163 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w3d7-review.yaml +115 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w4d1-ranges.yaml +123 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w4d2-sort-filter.yaml +138 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w4d3-regex.yaml +24 -92
- vimniscient-0.2.2/src/vimniscient/lessons/w4d4-norm.yaml +91 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w4d5-motions.yaml +7 -67
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w4d6-indent.yaml +19 -77
- vimniscient-0.2.2/src/vimniscient/lessons/w4d7-review.yaml +101 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w5d1-arguments.yaml +83 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w5d2-any-quote-bracket.yaml +5 -58
- vimniscient-0.2.2/src/vimniscient/lessons/w5d3-functions.yaml +111 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w5d4-small-objects.yaml +108 -0
- vimniscient-0.2.2/src/vimniscient/lessons/w5d5-comments.yaml +97 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w5d6-review.yaml +1 -18
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w6d1-code.yaml +1 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w6d2-notes.yaml +1 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons/w6d3-data.yaml +1 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/lessons.py +5 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/nvim/runner.lua +34 -24
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/progress.py +17 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/session.py +6 -3
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/today.py +3 -1
- vimniscient-0.2.2/src/vimniscient/transfer.py +56 -0
- vimniscient-0.2.2/src/vimniscient/transfer.yaml +184 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/ui/builtin_lessons.py +15 -1
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/ui/lesson.py +17 -2
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/ui/main.py +15 -2
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/check_answers.py +2 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/check_app.py +28 -0
- vimniscient-0.2.2/tests/check_diagnose.py +59 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/check_generate.py +1 -1
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/check_lessons.py +4 -1
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/check_today.py +12 -0
- vimniscient-0.2.2/tests/check_transfer.py +65 -0
- vimniscient-0.2.0/src/vimniscient/generate.py +0 -102
- vimniscient-0.2.0/src/vimniscient/lessons/w1d2-pairs.yaml +0 -188
- vimniscient-0.2.0/src/vimniscient/lessons/w1d3-sentences.yaml +0 -247
- vimniscient-0.2.0/src/vimniscient/lessons/w1d4-find.yaml +0 -184
- vimniscient-0.2.0/src/vimniscient/lessons/w1d5-lines.yaml +0 -249
- vimniscient-0.2.0/src/vimniscient/lessons/w1d6-counts.yaml +0 -351
- vimniscient-0.2.0/src/vimniscient/lessons/w2d1-dot.yaml +0 -195
- vimniscient-0.2.0/src/vimniscient/lessons/w2d3-cgn.yaml +0 -196
- vimniscient-0.2.0/src/vimniscient/lessons/w2d4-case-numbers.yaml +0 -201
- vimniscient-0.2.0/src/vimniscient/lessons/w2d5-insert.yaml +0 -229
- vimniscient-0.2.0/src/vimniscient/lessons/w2d6-visual.yaml +0 -206
- vimniscient-0.2.0/src/vimniscient/lessons/w2d7-review.yaml +0 -197
- vimniscient-0.2.0/src/vimniscient/lessons/w3d2-macros-more.yaml +0 -170
- vimniscient-0.2.0/src/vimniscient/lessons/w3d5-visual-block.yaml +0 -201
- vimniscient-0.2.0/src/vimniscient/lessons/w3d6-substitute.yaml +0 -290
- vimniscient-0.2.0/src/vimniscient/lessons/w3d7-review.yaml +0 -204
- vimniscient-0.2.0/src/vimniscient/lessons/w4d1-ranges.yaml +0 -214
- vimniscient-0.2.0/src/vimniscient/lessons/w4d2-sort-filter.yaml +0 -243
- vimniscient-0.2.0/src/vimniscient/lessons/w4d4-norm.yaml +0 -158
- vimniscient-0.2.0/src/vimniscient/lessons/w4d7-review.yaml +0 -175
- vimniscient-0.2.0/src/vimniscient/lessons/w5d1-arguments.yaml +0 -133
- vimniscient-0.2.0/src/vimniscient/lessons/w5d3-functions.yaml +0 -193
- vimniscient-0.2.0/src/vimniscient/lessons/w5d4-small-objects.yaml +0 -185
- vimniscient-0.2.0/src/vimniscient/lessons/w5d5-comments.yaml +0 -161
- {vimniscient-0.2.0 → vimniscient-0.2.2}/.gitignore +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/LICENSE +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/extras/nvim/vimniscient.lua +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/__init__.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/__main__.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/cards/foot.yaml +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/cards/zathura.yaml +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/cli.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/drills/omarchy.yaml +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/drills.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/keys.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/mnemonics.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/mnemonics.yaml +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/nvim/keymaps.lua +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/reminders.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/settings.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/srs.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/ui/__init__.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/ui/cards.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/ui/common.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/ui/drills.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/ui/stats.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/src/vimniscient/vimgolf.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/check_drills.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/check_lessons.lua +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/check_timing.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/live_drills.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/live_endless.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/live_lessons.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/live_retry.py +0 -0
- {vimniscient-0.2.0 → vimniscient-0.2.2}/tests/live_review.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vimniscient
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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
|
|
@@ -301,6 +301,8 @@ uv run python tests/check_lessons.py --lazyvim # LazyVim lessons, in your own c
|
|
|
301
301
|
uv run python tests/check_answers.py # card answers written different ways
|
|
302
302
|
uv run python tests/check_timing.py # task times: old progress databases upgrade safely
|
|
303
303
|
uv run python tests/check_generate.py # generated texts: reproducible, and they solve in Neovim
|
|
304
|
+
uv run python tests/check_diagnose.py # strategy feedback: no reference solution sets it off
|
|
305
|
+
uv run python tests/check_transfer.py # Choose: every way solves it in Neovim, nothing graded
|
|
304
306
|
uv run python tests/check_today.py # the daily budget, SRS scheduling, card introductions, hardtime
|
|
305
307
|
uv run python tests/check_app.py # every screen, driven headless
|
|
306
308
|
uv run python tests/live_lessons.py --all # types every solution into the real runner (needs tmux)
|
|
@@ -278,6 +278,8 @@ uv run python tests/check_lessons.py --lazyvim # LazyVim lessons, in your own c
|
|
|
278
278
|
uv run python tests/check_answers.py # card answers written different ways
|
|
279
279
|
uv run python tests/check_timing.py # task times: old progress databases upgrade safely
|
|
280
280
|
uv run python tests/check_generate.py # generated texts: reproducible, and they solve in Neovim
|
|
281
|
+
uv run python tests/check_diagnose.py # strategy feedback: no reference solution sets it off
|
|
282
|
+
uv run python tests/check_transfer.py # Choose: every way solves it in Neovim, nothing graded
|
|
281
283
|
uv run python tests/check_today.py # the daily budget, SRS scheduling, card introductions, hardtime
|
|
282
284
|
uv run python tests/check_app.py # every screen, driven headless
|
|
283
285
|
uv run python tests/live_lessons.py --all # types every solution into the real runner (needs tmux)
|
|
@@ -40,7 +40,7 @@ def _norm_token(match: re.Match) -> str:
|
|
|
40
40
|
|
|
41
41
|
def norm_keys(text: str, case_sensitive: bool = True) -> str:
|
|
42
42
|
s = text.strip()
|
|
43
|
-
s = re.sub(r"(?i)^(leader|space|spc)(\s+|$)", "<leader>", s)
|
|
43
|
+
s = re.sub(r"(?i)^(leader|space|spc)(\s*\+\s*|\s+|$)", "<leader>", s)
|
|
44
44
|
s = re.sub(r"(?i)(?<![<\w])space(?![>\w])", "<Space>", s)
|
|
45
45
|
s = re.sub(r"(?i)\b(?:ctrl|control)[-+ ]+(\S)", lambda m: f"<C-{m.group(1)}>", s)
|
|
46
46
|
s = re.sub(r"(?i)\b(?:alt|meta)[-+ ]+(\S)", lambda m: f"<M-{m.group(1)}>", s)
|
|
@@ -55,7 +55,7 @@ _MOD_ALIASES = {
|
|
|
55
55
|
"SUPER": "SUPER", "WIN": "SUPER", "CMD": "SUPER", "MOD": "SUPER", "LOGO": "SUPER",
|
|
56
56
|
"CTRL": "CTRL", "CONTROL": "CTRL", "CTL": "CTRL",
|
|
57
57
|
"ALT": "ALT", "OPT": "ALT", "OPTION": "ALT", "MOD1": "ALT", "META": "ALT",
|
|
58
|
-
"SHIFT": "SHIFT", "PREFIX": "PREFIX", "COPYMODE": "COPYMODE",
|
|
58
|
+
"SHIFT": "SHIFT", "PREFIX": "PREFIX", "LEADER": "PREFIX", "COPYMODE": "COPYMODE",
|
|
59
59
|
}
|
|
60
60
|
_KEY_ALIASES = {
|
|
61
61
|
"ENTER": "RETURN", "CR": "RETURN", "ESC": "ESCAPE", "DEL": "DELETE", "INS": "INSERT",
|
|
@@ -11,13 +11,14 @@ from dataclasses import replace
|
|
|
11
11
|
|
|
12
12
|
from textual.app import App
|
|
13
13
|
from textual.color import Color
|
|
14
|
+
from textual.content import Content
|
|
14
15
|
|
|
15
|
-
from vimniscient import drills, endless, reminders, today, vimgolf
|
|
16
|
+
from vimniscient import drills, endless, reminders, today, transfer, vimgolf
|
|
16
17
|
from vimniscient.cards import has_lazyvim, load_decks
|
|
17
18
|
from vimniscient.lessons import Lesson, load_lessons, two_rounds
|
|
18
19
|
from vimniscient.progress import Progress
|
|
19
20
|
from vimniscient.session import COLORS, TaskResult, mouse_wheel, run_endless, run_lesson
|
|
20
|
-
from vimniscient.ui.builtin_lessons import endless_lesson, placement_lesson, review_lesson, vimgolf_lesson
|
|
21
|
+
from vimniscient.ui.builtin_lessons import endless_lesson, placement_lesson, review_lesson, transfer_lesson, vimgolf_lesson
|
|
21
22
|
from vimniscient.ui.cards import CardScreen
|
|
22
23
|
from vimniscient.ui.common import CATPPUCCIN, DEFAULT_THEME, omarchy_theme, plural
|
|
23
24
|
from vimniscient.ui.drills import DrillScreen
|
|
@@ -44,7 +45,7 @@ class VimniscientApp(App):
|
|
|
44
45
|
|
|
45
46
|
#main { height: 1fr; }
|
|
46
47
|
#left { width: 46; max-width: 45%; }
|
|
47
|
-
#plan { height:
|
|
48
|
+
#plan { height: 9; }
|
|
48
49
|
#lesson-list { height: 1fr; }
|
|
49
50
|
#right { width: 1fr; }
|
|
50
51
|
#detail-pane { height: 1fr; }
|
|
@@ -74,7 +75,7 @@ class VimniscientApp(App):
|
|
|
74
75
|
#results-bar { height: 1; }
|
|
75
76
|
#note { padding: 0 0 1 0; color: $success; height: auto; }
|
|
76
77
|
#results-list { height: auto; padding-right: 1; }
|
|
77
|
-
#results-why { height: auto; padding: 1 0 0 0; }
|
|
78
|
+
#results-why, #results-advice { height: auto; padding: 1 0 0 0; }
|
|
78
79
|
|
|
79
80
|
#stats-view { height: 1fr; padding: 0 2; }
|
|
80
81
|
.section-label { color: $text-muted; text-style: bold; height: 1; margin-top: 1; }
|
|
@@ -121,7 +122,7 @@ class VimniscientApp(App):
|
|
|
121
122
|
def start_lesson(
|
|
122
123
|
self,
|
|
123
124
|
lesson: Lesson,
|
|
124
|
-
on_results: Callable[[list[TaskResult]], str] | None = None,
|
|
125
|
+
on_results: Callable[[list[TaskResult]], str | Content] | None = None,
|
|
125
126
|
done: Callable[[], None] = lambda: None,
|
|
126
127
|
) -> None:
|
|
127
128
|
"""Run a lesson in Neovim, then show the results; `done` runs when the user moves on.
|
|
@@ -188,6 +189,10 @@ class VimniscientApp(App):
|
|
|
188
189
|
elif step_id == "vimgolf":
|
|
189
190
|
if not progress.vimgolf_done_today():
|
|
190
191
|
return self.start_vimgolf
|
|
192
|
+
elif step_id == "transfer":
|
|
193
|
+
picks = transfer.pick(self.lessons, progress)
|
|
194
|
+
if picks:
|
|
195
|
+
return lambda done: self.start_transfer(picks, done)
|
|
191
196
|
elif step_id == "endless":
|
|
192
197
|
start = progress.endless_start(endless.RESTART_BELOW)
|
|
193
198
|
if direct:
|
|
@@ -252,6 +257,19 @@ class VimniscientApp(App):
|
|
|
252
257
|
def action_endless(self) -> None:
|
|
253
258
|
self.run_step("endless", lambda: None)
|
|
254
259
|
|
|
260
|
+
def action_transfer(self) -> None:
|
|
261
|
+
self.run_step("transfer", lambda: None)
|
|
262
|
+
|
|
263
|
+
def start_transfer(self, picks: list[transfer.Challenge], done: Callable[[], None]) -> None:
|
|
264
|
+
def on_results(results: list[TaskResult]) -> Content:
|
|
265
|
+
self.progress.record_transfer(results)
|
|
266
|
+
by_goal = {c.task.goal: c for c in picks}
|
|
267
|
+
lines = [f"{r.task.goal} {' or '.join(ways)}" for r in results
|
|
268
|
+
if (ways := transfer.another_way(by_goal[r.task.goal], r.keys, r.status == "solved"))]
|
|
269
|
+
return Content("Another way\n" + "\n".join(lines)) if lines else Content("")
|
|
270
|
+
|
|
271
|
+
self.start_lesson(transfer_lesson([c.task for c in picks]), on_results, done)
|
|
272
|
+
|
|
255
273
|
def start_endless(self, start: int, done: Callable[[], None]) -> None:
|
|
256
274
|
"""Climb from `start` until the user quits, then show how it went."""
|
|
257
275
|
best = self.progress.endless_best()
|
|
@@ -12,7 +12,7 @@ cards:
|
|
|
12
12
|
- {prompt: Grep the project (root dir), answer: [<leader>/, <leader>sg]}
|
|
13
13
|
- {prompt: Switch buffer, answer: ['<leader>,', '<leader>fb']}
|
|
14
14
|
- {prompt: Recent files, answer: [<leader>fr]}
|
|
15
|
-
- {prompt: File explorer (neo-tree), answer: [<leader>e]}
|
|
15
|
+
- {prompt: File explorer (neo-tree), answer: [<leader>e], note: 'e is explorer.'}
|
|
16
16
|
- {prompt: Next buffer, answer: [<S-l>, ']b']}
|
|
17
17
|
- {prompt: Previous buffer, answer: [<S-h>, '[b']}
|
|
18
18
|
- {prompt: Delete the buffer, answer: [<leader>bd]}
|
|
@@ -5,9 +5,9 @@ case_sensitive: false
|
|
|
5
5
|
note: Readline keys work in bash and in most terminal prompts.
|
|
6
6
|
cards:
|
|
7
7
|
- {prompt: Search back through your command history, answer: [<C-r>]}
|
|
8
|
-
- {prompt: Jump to the start of the line, answer: [<C-a>, <Home>]}
|
|
9
|
-
- {prompt: Jump to the end of the line, answer: [<C-e>, <End>]}
|
|
10
|
-
- {prompt: Delete the word before the cursor, answer: [<C-w>]}
|
|
8
|
+
- {prompt: Jump to the start of the line, answer: [<C-a>, <Home>], note: 'a is the start of the alphabet. The shell isn’t Vim: 0 and $ just type themselves.'}
|
|
9
|
+
- {prompt: Jump to the end of the line, answer: [<C-e>, <End>], note: 'e is end. The shell isn’t Vim: $ just types itself.'}
|
|
10
|
+
- {prompt: Delete the word before the cursor, answer: [<C-w>], note: 'w is word.'}
|
|
11
11
|
- {prompt: Delete from the cursor to the start of the line, answer: [<C-u>]}
|
|
12
12
|
- {prompt: Delete from the cursor to the end of the line, answer: [<C-k>]}
|
|
13
13
|
- {prompt: Paste what you just deleted with Ctrl-w/u/k, answer: [<C-y>]}
|
|
@@ -7,8 +7,8 @@ case_sensitive: true
|
|
|
7
7
|
cards:
|
|
8
8
|
# week 1: text objects, find/till, lines, counts
|
|
9
9
|
- {week: 1, day: 1, prompt: 'Change the whole word under the cursor, wherever in it the cursor is', answer: [ciw], note: 'cw only changes from the cursor to the end of the word.'}
|
|
10
|
-
- {week: 1, day: 1,
|
|
11
|
-
- {week: 1, day: 1,
|
|
10
|
+
- {week: 1, day: 1, id: 'vim:Delete a word and the space after it', prompt: 'Delete the whole word under the cursor, wherever in it the cursor is, and the space after it', answer: [daw], note: 'dw only deletes from the cursor on.'}
|
|
11
|
+
- {week: 1, day: 1, id: 'vim:Change a WORD (space to space, punctuation included)', prompt: 'Change the whole WORD under the cursor (space to space, punctuation included), keeping the space after it', answer: [ciW]}
|
|
12
12
|
- {week: 1, day: 1, prompt: Yank the word under the cursor, answer: [yiw]}
|
|
13
13
|
- {week: 1, day: 2, prompt: Change everything inside the double quotes, answer: ['ci"']}
|
|
14
14
|
- {week: 1, day: 2, prompt: Delete the quotes and what is inside them, answer: ['da"']}
|
|
@@ -77,7 +77,7 @@ def _static_decks() -> dict[str, Deck]:
|
|
|
77
77
|
deck.cards.append(
|
|
78
78
|
Card(
|
|
79
79
|
deck=deck.id,
|
|
80
|
-
id=f"{deck.id}:{c['prompt']}",
|
|
80
|
+
id=c.get("id", f"{deck.id}:{c['prompt']}"), # `id` keeps a reworded card's history
|
|
81
81
|
prompt=c["prompt"],
|
|
82
82
|
answers=[str(a) for a in c["answer"]],
|
|
83
83
|
style=data.get("style", "keys"),
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""What your own keys say about your strategy, on the results screen.
|
|
2
|
+
|
|
3
|
+
A few rules, each one certain from the keys alone, so that a task with nothing
|
|
4
|
+
clear to say says nothing. They only speak when you went over the reference,
|
|
5
|
+
and never about text you typed in insert mode (tests/check_diagnose.py checks
|
|
6
|
+
that no reference solution sets one off).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from vimniscient.keys import split_keys
|
|
12
|
+
|
|
13
|
+
MOTION_RUN = 4 # jjjj; Vjj in a reference is fine, and three presses aren't a habit
|
|
14
|
+
X_RUN = 3
|
|
15
|
+
MOTIONS = {"h", "j", "k", "l", "w", "b", "e"}
|
|
16
|
+
OPERATORS = {"c", "d", "y", ">", "<", "="}
|
|
17
|
+
TAKES_CHAR = {"f", "t", "F", "T", "r", "m", "'", "`", "@", '"'}
|
|
18
|
+
INSERTS = {"i", "a", "A", "I", "o", "O", "s", "S", "C", "R"}
|
|
19
|
+
_REPEAT = re.compile(r"((?:<[^<>\s]+>|.){3,}?)\1\1", re.S)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _skip_line(tokens: list[str], i: int) -> int:
|
|
23
|
+
while i < len(tokens) and tokens[i] != "<CR>":
|
|
24
|
+
i += 1
|
|
25
|
+
return i + 1
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _skip_insert(tokens: list[str], i: int) -> int:
|
|
29
|
+
while i < len(tokens) and tokens[i] != "<Esc>":
|
|
30
|
+
i += 1
|
|
31
|
+
return i + 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _skip_motion(tokens: list[str], i: int) -> int:
|
|
35
|
+
"""Past what follows an operator: a count, then a motion or a text object."""
|
|
36
|
+
while i < len(tokens) and tokens[i].isdigit():
|
|
37
|
+
i += 1
|
|
38
|
+
if i >= len(tokens):
|
|
39
|
+
return i
|
|
40
|
+
key = tokens[i]
|
|
41
|
+
if key in ("/", "?"):
|
|
42
|
+
return _skip_line(tokens, i + 1)
|
|
43
|
+
return i + (2 if key in {"i", "a", "g", "f", "t", "F", "T", "'", "`"} else 1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def normal_keys(keys: str) -> list[list[str]]:
|
|
47
|
+
"""The runs of keys pressed as commands: not typed text, search or `:` lines."""
|
|
48
|
+
tokens, runs, run = split_keys(keys), [], []
|
|
49
|
+
i, visual, recording = 0, False, False
|
|
50
|
+
while i < len(tokens):
|
|
51
|
+
key = tokens[i]
|
|
52
|
+
i += 1
|
|
53
|
+
if key in (":", "/", "?"):
|
|
54
|
+
i, visual = _skip_line(tokens, i), False
|
|
55
|
+
elif visual and key in {"c", "s", "I", "A"}:
|
|
56
|
+
i, visual = _skip_insert(tokens, i), False
|
|
57
|
+
elif visual and key in OPERATORS | {"x", "J", "U", "u", "<Esc>"}:
|
|
58
|
+
visual = False
|
|
59
|
+
elif key in OPERATORS:
|
|
60
|
+
if tokens[i:i + 1] == [key]: # dd, cc, yy
|
|
61
|
+
i += 1
|
|
62
|
+
else:
|
|
63
|
+
i = _skip_motion(tokens, i)
|
|
64
|
+
if key == "c":
|
|
65
|
+
i = _skip_insert(tokens, i)
|
|
66
|
+
elif key == "g" and tokens[i:i + 1] and tokens[i] in {"U", "u", "~"}:
|
|
67
|
+
i = _skip_motion(tokens, i + 1)
|
|
68
|
+
elif key == "g":
|
|
69
|
+
i += 1
|
|
70
|
+
elif key in TAKES_CHAR:
|
|
71
|
+
i += 1
|
|
72
|
+
elif key == "q":
|
|
73
|
+
i += 0 if recording else 1
|
|
74
|
+
recording = not recording
|
|
75
|
+
elif key in INSERTS:
|
|
76
|
+
i = _skip_insert(tokens, i)
|
|
77
|
+
else:
|
|
78
|
+
visual = visual or key in {"v", "V", "<C-v>"}
|
|
79
|
+
run.append(key)
|
|
80
|
+
continue
|
|
81
|
+
if run:
|
|
82
|
+
runs.append(run)
|
|
83
|
+
run = []
|
|
84
|
+
return [*runs, run] if run else runs
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _longest_run(runs: list[list[str]], wanted: set[str]) -> tuple[str, int]:
|
|
88
|
+
best, length = "", 0
|
|
89
|
+
for tokens in runs:
|
|
90
|
+
last, n = "", 0
|
|
91
|
+
for key in tokens:
|
|
92
|
+
n = n + 1 if key == last else 1
|
|
93
|
+
last = key
|
|
94
|
+
if key in wanted and n > length:
|
|
95
|
+
best, length = key, n
|
|
96
|
+
return best, length
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def diagnose(keys: str, count: int, ref: int) -> str:
|
|
100
|
+
"""One thing worth saying about how `keys` went about it, or nothing."""
|
|
101
|
+
if count <= ref:
|
|
102
|
+
return ""
|
|
103
|
+
runs = normal_keys(keys)
|
|
104
|
+
motion, length = _longest_run(runs, MOTIONS)
|
|
105
|
+
if length >= MOTION_RUN:
|
|
106
|
+
better = "f, t, / or a count" if motion in "hl" else "a count"
|
|
107
|
+
return f"{motion} {length} times in a row: {better} gets there in one go."
|
|
108
|
+
if _longest_run(runs, {"x"})[1] >= X_RUN:
|
|
109
|
+
return "x over and over: a count, dw or diw takes it all at once."
|
|
110
|
+
repeat = _REPEAT.search(keys)
|
|
111
|
+
if repeat and not any("." in run for run in runs) and "<Esc>" in repeat.group(1):
|
|
112
|
+
return f"You typed {repeat.group(1)} three times: . repeats the last change."
|
|
113
|
+
return ""
|
|
@@ -45,7 +45,7 @@ def ladder(lessons: list[Lesson], start: int, length: int = RUNGS, seed: object
|
|
|
45
45
|
lesson = mix[-1] if rng.random() < 0.5 else rng.choice(mix)
|
|
46
46
|
task = rng.choice([t for t in lesson.tasks if t.item_id not in recent] or lesson.tasks)
|
|
47
47
|
recent.append(task.item_id)
|
|
48
|
-
rungs.append((task, rng.randrange(task.
|
|
48
|
+
rungs.append((task, rng.randrange(task.choices)))
|
|
49
49
|
return rungs
|
|
50
50
|
|
|
51
51
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Practice texts made from a seed, for skills where any sentence will do.
|
|
2
|
+
|
|
3
|
+
A lesson task can name a `generate` kind. Its Learn round keeps the authored
|
|
4
|
+
text; Recall and reviews can get a generated one instead, so a skill gets
|
|
5
|
+
practised on sentences you haven't seen. The same seed always gives the same
|
|
6
|
+
task, so a run can be replayed and every kind can be checked in Neovim
|
|
7
|
+
(tests/check_generate.py).
|
|
8
|
+
|
|
9
|
+
The sentences stay mnemonic: they're the hooks from mnemonics.yaml, what keys
|
|
10
|
+
stand for.
|
|
11
|
+
|
|
12
|
+
Kinds:
|
|
13
|
+
ciw-typo two letters of a word swapped; ciw and type it right
|
|
14
|
+
daw-double a word doubled; daw deletes one copy and its space
|
|
15
|
+
ciW-dotted a word written with dots between its letters; ciW rewrites it
|
|
16
|
+
ci-quote a key's meaning, wrong, in double quotes; ci" fixes it
|
|
17
|
+
di-paren the arguments of a call; di( empties it
|
|
18
|
+
da-paren a call's arguments; da( takes the parentheses too
|
|
19
|
+
di-bracket a list of keys; di[ empties it
|
|
20
|
+
dt-letter filler before a word; dt and the word's first letter
|
|
21
|
+
D-rest filler after a sentence; D deletes to the end of the line
|
|
22
|
+
J-split a sentence over two lines; J joins them
|
|
23
|
+
ddp-swap two lines the wrong way round; ddp swaps them
|
|
24
|
+
count-dw a few filler words; a count on dw
|
|
25
|
+
count-dd a few filler lines; a count on dd
|
|
26
|
+
count-x stray x letters; a count on x
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import random
|
|
30
|
+
import re
|
|
31
|
+
from dataclasses import replace
|
|
32
|
+
from functools import cache
|
|
33
|
+
from importlib import resources
|
|
34
|
+
|
|
35
|
+
import yaml
|
|
36
|
+
|
|
37
|
+
from vimniscient.lessons import Task, load_lessons
|
|
38
|
+
from vimniscient.mnemonics import _tables
|
|
39
|
+
from vimniscient.mnemonics import hooks as key_hooks
|
|
40
|
+
|
|
41
|
+
FILLER = ("um", "well", "like", "so")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cache
|
|
45
|
+
def sentences() -> list[str]:
|
|
46
|
+
"""The hooks of the keys week 1 teaches: nothing about keys you haven't met yet."""
|
|
47
|
+
known = {hook for lesson in load_lessons() if lesson.week == 1 for key in lesson.keys for hook in key_hooks(key)}
|
|
48
|
+
hooks = [hook for hook in dict.fromkeys(hook for table in _tables()[:2] for hook in table.values()) if hook in known]
|
|
49
|
+
# as written: a capital would change the key ("u is down" is not "U is down")
|
|
50
|
+
return [h + ("" if h.endswith(".") else ".") for h in hooks]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@cache
|
|
54
|
+
def meanings() -> list[tuple[str, str]]:
|
|
55
|
+
"""Single-letter keys with a one-word meaning: (c, change), (d, delete) ..."""
|
|
56
|
+
data = yaml.safe_load((resources.files("vimniscient") / "mnemonics.yaml").read_text(encoding="utf-8"))
|
|
57
|
+
known = {s.removesuffix(".") for s in sentences()}
|
|
58
|
+
return [(key, entry["means"]) for key, entry in data.items() if entry["hook"].removesuffix(".") in known
|
|
59
|
+
and re.fullmatch(r"[a-zA-Z]", key) and re.fullmatch(r"[a-z]+", entry.get("means") or "")]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_WORD = re.compile(r"(?<![\w'.-])[A-Za-z]{4,}(?![\w'-])")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _words(sentence: str, spaced: bool) -> list[re.Match]:
|
|
66
|
+
"""Plain words of four letters or more; `spaced` wants a space on both sides (a WORD)."""
|
|
67
|
+
found = list(_WORD.finditer(sentence))
|
|
68
|
+
if spaced:
|
|
69
|
+
found = [m for m in found if m.start() > 0 and sentence[m.start() - 1] == " "
|
|
70
|
+
and m.end() < len(sentence) and sentence[m.end()] == " "]
|
|
71
|
+
else: # daw and ciw want the word to have a space after it, or anything but a letter
|
|
72
|
+
found = [m for m in found if m.end() < len(sentence)]
|
|
73
|
+
return found
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _gaps(sentence: str) -> list[int]:
|
|
77
|
+
"""Where a new word can go: the start of every word but the first."""
|
|
78
|
+
return [m.end() for m in re.finditer(r" (?=[A-Za-z])", sentence)]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _ci_quote(rng: random.Random) -> dict:
|
|
82
|
+
(key, right), (_, wrong) = rng.sample(meanings(), 2)
|
|
83
|
+
start = f'{key}_means = "{wrong}"'
|
|
84
|
+
return dict(goal=f'{key} stands for {right}: fix the string.', start=[start], cursor=(1, rng.randrange(len(start) - 1)),
|
|
85
|
+
target=[f'{key}_means = "{right}"'], solution=f'ci"{right}<Esc>',
|
|
86
|
+
hint='ci" changes what is inside the quotes, and finds them from anywhere before them.')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _paren(rng: random.Random, around: bool) -> dict:
|
|
90
|
+
name = rng.choice(("around", "parens") if around else ("inner", "inside"))
|
|
91
|
+
args = ", ".join(means for _, means in rng.sample(meanings(), 3))
|
|
92
|
+
start = f"{name}({args}) + 1"
|
|
93
|
+
target, keys = (f"{name} + 1", "da(") if around else (f"{name}() + 1", "di(")
|
|
94
|
+
what = "the call's parentheses and arguments" if around else "the arguments, keeping the parentheses"
|
|
95
|
+
return dict(goal=f"Delete {what}.", start=[start], cursor=(1, len(name) + 1 + rng.randrange(len(args))), target=[target],
|
|
96
|
+
solution=keys, hint=f"{keys} works from anywhere inside the parentheses.")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _di_bracket(rng: random.Random) -> dict:
|
|
100
|
+
name = rng.choice(("keys", "square", "bracket"))
|
|
101
|
+
items = ", ".join(f'"{key}"' for key, _ in rng.sample(meanings(), 3))
|
|
102
|
+
start = f"{name} = [{items}]"
|
|
103
|
+
return dict(goal="Empty the list, keeping the square brackets.", start=[start], cursor=(1, len(name) + 4 + rng.randrange(len(items))),
|
|
104
|
+
target=[f"{name} = []"], solution="di[", hint="[ names the square brackets: di[ empties them.")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _dt_letter(rng: random.Random) -> dict:
|
|
108
|
+
filler = " ".join(rng.sample(FILLER, 2)) + " "
|
|
109
|
+
while True:
|
|
110
|
+
sentence = rng.choice(sentences())
|
|
111
|
+
gaps = [g for g in _gaps(sentence) if sentence[g] not in filler]
|
|
112
|
+
if gaps:
|
|
113
|
+
break
|
|
114
|
+
gap = rng.choice(gaps)
|
|
115
|
+
word = sentence[gap:].split(" ")[0].rstrip(".,:;")
|
|
116
|
+
letter = word[0]
|
|
117
|
+
return dict(goal=f'Delete everything up to "{word}".', start=[sentence[:gap] + filler + sentence[gap:]], cursor=(1, gap),
|
|
118
|
+
target=[sentence], solution=f"dt{letter}", hint=f"dt{letter} deletes till the next {letter}, and stops just before it.")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _d_rest(rng: random.Random) -> dict:
|
|
122
|
+
sentence = rng.choice(sentences())
|
|
123
|
+
return dict(goal="Delete the filler after the full stop.", start=[f"{sentence} {' '.join(rng.sample(FILLER, 3))}"],
|
|
124
|
+
cursor=(1, len(sentence)), target=[sentence], solution="D", hint="D is a big d: it deletes to the end of the line.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _j_split(rng: random.Random) -> dict:
|
|
128
|
+
sentence = rng.choice([s for s in sentences() if _gaps(s)])
|
|
129
|
+
gap = rng.choice(_gaps(sentence))
|
|
130
|
+
return dict(goal="Join the two lines.", start=[sentence[:gap - 1], sentence[gap:]], cursor=(1, 0), target=[sentence],
|
|
131
|
+
solution="J", hint="J joins this line and the next, with one space.")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _ddp_swap(rng: random.Random) -> dict:
|
|
135
|
+
first, second = rng.sample(sentences(), 2)
|
|
136
|
+
return dict(goal="Swap the two lines.", start=[first, second], cursor=(1, 0), target=[second, first],
|
|
137
|
+
solution="ddp", hint="dd cuts the line, p puts it below the next one.")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _count_dw(rng: random.Random) -> dict:
|
|
141
|
+
n = rng.randint(2, 4)
|
|
142
|
+
sentence = rng.choice([s for s in sentences() if _gaps(s)])
|
|
143
|
+
gap = rng.choice(_gaps(sentence))
|
|
144
|
+
filler = " ".join(rng.choice(FILLER) for _ in range(n)) + " "
|
|
145
|
+
return dict(goal=f"Delete the {n} filler words.", start=[sentence[:gap] + filler + sentence[gap:]], cursor=(1, gap),
|
|
146
|
+
target=[sentence], solution=f"{n}dw", hint=f"A count repeats the verb: {n}dw deletes {n} words.")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _count_dd(rng: random.Random) -> dict:
|
|
150
|
+
n = rng.randint(2, 4)
|
|
151
|
+
first, last = rng.sample(sentences(), 2)
|
|
152
|
+
return dict(goal=f"Delete the {n} filler lines.", start=[first, *[rng.choice(FILLER) for _ in range(n)], last], cursor=(2, 0),
|
|
153
|
+
target=[first, last], solution=f"{n}dd", hint=f"A count works on dd: {n}dd deletes {n} lines.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _count_x(rng: random.Random) -> dict:
|
|
157
|
+
n = rng.randint(2, 7)
|
|
158
|
+
sentence = rng.choice([s for s in sentences() if _gaps(s)])
|
|
159
|
+
gap = rng.choice(_gaps(sentence))
|
|
160
|
+
return dict(goal=f"Delete the {n} x letters.", start=[sentence[:gap] + "x" * n + sentence[gap:]], cursor=(1, gap),
|
|
161
|
+
target=[sentence], solution=f"{n}x", hint=f"x crosses out a character; {n}x crosses out {n}.")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
MAKERS = {
|
|
165
|
+
"ci-quote": _ci_quote,
|
|
166
|
+
"di-paren": lambda rng: _paren(rng, around=False),
|
|
167
|
+
"da-paren": lambda rng: _paren(rng, around=True),
|
|
168
|
+
"di-bracket": _di_bracket,
|
|
169
|
+
"dt-letter": _dt_letter,
|
|
170
|
+
"D-rest": _d_rest,
|
|
171
|
+
"J-split": _j_split,
|
|
172
|
+
"ddp-swap": _ddp_swap,
|
|
173
|
+
"count-dw": _count_dw,
|
|
174
|
+
"count-dd": _count_dd,
|
|
175
|
+
"count-x": _count_x,
|
|
176
|
+
}
|
|
177
|
+
KINDS = ("ciw-typo", "daw-double", "ciW-dotted", *MAKERS)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def generate(kind: str, seed: str, parent: Task) -> Task:
|
|
181
|
+
"""A `kind` task from `seed`, with the parent task's hint and place in its lesson."""
|
|
182
|
+
rng = random.Random(f"{kind}:{seed}")
|
|
183
|
+
if kind in MAKERS:
|
|
184
|
+
return replace(parent, ft="", variants=[], generated=seed, **MAKERS[kind](rng))
|
|
185
|
+
while True:
|
|
186
|
+
sentence = rng.choice(sentences())
|
|
187
|
+
words = _words(sentence, spaced=kind == "ciW-dotted")
|
|
188
|
+
if kind == "daw-double":
|
|
189
|
+
words = [m for m in words if sentence[m.end()] == " "]
|
|
190
|
+
if kind == "ciw-typo":
|
|
191
|
+
words = [m for m in words if len(set(m.group()[1:-1])) > 1]
|
|
192
|
+
if words:
|
|
193
|
+
break
|
|
194
|
+
m = rng.choice(words)
|
|
195
|
+
word, before, after = m.group(), sentence[: m.start()], sentence[m.end():]
|
|
196
|
+
if kind == "ciw-typo":
|
|
197
|
+
while True:
|
|
198
|
+
i = rng.randrange(1, len(word) - 2)
|
|
199
|
+
if word[i] != word[i + 1]:
|
|
200
|
+
break
|
|
201
|
+
shown = word[:i] + word[i + 1] + word[i] + word[i + 2:]
|
|
202
|
+
start, cursor = before + shown + after, len(before) + rng.randrange(len(shown))
|
|
203
|
+
goal, solution = f'Fix "{shown}" to "{word}".', f"ciw{word}<Esc>"
|
|
204
|
+
elif kind == "daw-double":
|
|
205
|
+
start = before + word + " " + word + after
|
|
206
|
+
cursor = len(before) + rng.choice([0, len(word) + 1]) + rng.randrange(len(word))
|
|
207
|
+
goal, solution = f'Delete the doubled "{word}".', "daw"
|
|
208
|
+
elif kind == "ciW-dotted":
|
|
209
|
+
shown = ".".join(word)
|
|
210
|
+
start, cursor = before + shown + after, len(before) + rng.randrange(len(shown))
|
|
211
|
+
goal, solution = f'Write the dotted "{shown}" as "{word}".', f"ciW{word}<Esc>"
|
|
212
|
+
else:
|
|
213
|
+
raise ValueError(f"no generator called {kind}")
|
|
214
|
+
return replace(parent, goal=goal, start=[start], cursor=(1, cursor), target=[sentence],
|
|
215
|
+
solution=solution, ft="", variants=[], generated=seed)
|
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
from vimniscient.cards import Card, Deck
|
|
17
17
|
from vimniscient.keys import split_keys
|
|
18
|
+
from vimniscient.mnemonics import OPERATORS
|
|
18
19
|
|
|
19
20
|
LOG = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local/state")) / "nvim" / "hardtime.nvim.log"
|
|
20
21
|
|
|
@@ -91,7 +92,7 @@ def habit_deck() -> Deck:
|
|
|
91
92
|
answers=[better],
|
|
92
93
|
style="keys",
|
|
93
94
|
case_sensitive=True,
|
|
94
|
-
note=f"hardtime caught this {habit.total}×.",
|
|
95
|
+
note=f"{'Verb first, then the motion. ' if better[0] in OPERATORS else ''}hardtime caught this {habit.total}×.",
|
|
95
96
|
order=i,
|
|
96
97
|
)
|
|
97
98
|
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Sources for the facts in the lesson texts
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`../mnemonics.yaml`)
|
|
5
|
-
Every factual claim
|
|
3
|
+
Lesson practice texts are mnemonic: each one is about the keys it practises (see
|
|
4
|
+
`../mnemonics.yaml`). Memory science belongs in lesson intros, where it explains
|
|
5
|
+
why the training works. Every factual claim there should trace back to one of these.
|
|
6
6
|
|
|
7
7
|
- **Atkinson & Raugh (1975).** An application of the mnemonic keyword method to
|
|
8
8
|
the acquisition of a Russian vocabulary. *JEP: Human Learning and Memory*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
week: 1
|
|
2
2
|
day: 1
|
|
3
3
|
title: Word text objects
|
|
4
|
+
skill: text-objects
|
|
4
5
|
intro: |
|
|
5
6
|
Read a Vim command as a small sentence: a verb, then a noun. The verbs are
|
|
6
7
|
`c` *change*, `d` *delete* and `y` *yank* (Vim's old word for copy). The noun
|
|
@@ -32,12 +33,6 @@ tasks:
|
|
|
32
33
|
target: |-
|
|
33
34
|
Change inner word: c is the verb, iw the noun, and the cursor sits anywhere in it.
|
|
34
35
|
solution: 'ciwanywhere<Esc>'
|
|
35
|
-
- goal: 'Fix "memroy" to "memory" (the cursor is in the middle of the word).'
|
|
36
|
-
start: |-
|
|
37
|
-
Testing yourself beats rereading: type ciw from mem▮roy, not from the page.
|
|
38
|
-
target: |-
|
|
39
|
-
Testing yourself beats rereading: type ciw from memory, not from the page.
|
|
40
|
-
solution: 'ciwmemory<Esc>'
|
|
41
36
|
- goal: 'Fix "undersocres" to "underscores" (the cursor is in the middle of the word).'
|
|
42
37
|
start: |-
|
|
43
38
|
In ciw, the w is a word: letters, digits and under▮socres, nothing else.
|
|
@@ -64,11 +59,6 @@ tasks:
|
|
|
64
59
|
daw takes a word and what is around it, its ▮its trailing space.
|
|
65
60
|
target: |-
|
|
66
61
|
daw takes a word and what is around it, its trailing space.
|
|
67
|
-
- goal: 'Delete one "himself".'
|
|
68
|
-
start: |-
|
|
69
|
-
Ebbinghaus tested ▮himself himself on nonsense syllables; daw cuts the echo.
|
|
70
|
-
target: |-
|
|
71
|
-
Ebbinghaus tested himself on nonsense syllables; daw cuts the echo.
|
|
72
62
|
- goal: 'Delete one "so".'
|
|
73
63
|
start: |-
|
|
74
64
|
With daw, a means around: the word goes, and ▮so so does one space.
|
|
@@ -101,12 +91,6 @@ tasks:
|
|
|
101
91
|
target: |-
|
|
102
92
|
ciW sees a path as one WORD, slashes and all.
|
|
103
93
|
solution: 'ciWa path<Esc>'
|
|
104
|
-
- goal: 'Replace "Fitts&Posner(1967)" with "Fitts-and-Posner".'
|
|
105
|
-
start: |-
|
|
106
|
-
ciW: from space to space, Fitts&Pos▮ner(1967) counts as one WORD.
|
|
107
|
-
target: |-
|
|
108
|
-
ciW: from space to space, Fitts-and-Posner counts as one WORD.
|
|
109
|
-
solution: 'ciWFitts-and-Posner<Esc>'
|
|
110
94
|
- goal: 'Spell the function name out as change_inner_word.'
|
|
111
95
|
ft: python
|
|
112
96
|
start: |-
|
|
@@ -180,18 +164,6 @@ tasks:
|
|
|
180
164
|
target: |-
|
|
181
165
|
viwp swaps a selection for the yank, so select, then put the yank
|
|
182
166
|
solution: 'yiw$viwp'
|
|
183
|
-
- goal: 'Copy "spaced" over "massed".'
|
|
184
|
-
start: |-
|
|
185
|
-
▮spaced reviews beat massed ones, so keep them massed
|
|
186
|
-
target: |-
|
|
187
|
-
spaced reviews beat massed ones, so keep them spaced
|
|
188
|
-
solution: 'yiw$viwp'
|
|
189
|
-
- goal: 'Copy "mixed" over the last "blocked".'
|
|
190
|
-
start: |-
|
|
191
|
-
Rohrer and Taylor: blocked practice feels better than ▮mixed, yet scores below blocked
|
|
192
|
-
target: |-
|
|
193
|
-
Rohrer and Taylor: blocked practice feels better than mixed, yet scores below mixed
|
|
194
|
-
solution: 'yiw$viwp'
|
|
195
167
|
- goal: 'Delete the doubled "the".'
|
|
196
168
|
start: |-
|
|
197
169
|
daw: delete a word, and ▮the the space after it.
|
|
@@ -206,16 +178,6 @@ tasks:
|
|
|
206
178
|
Doubled words ▮like like this one are what daw is for.
|
|
207
179
|
target: |-
|
|
208
180
|
Doubled words like this one are what daw is for.
|
|
209
|
-
- goal: 'Delete the doubled "four".'
|
|
210
|
-
start: |-
|
|
211
|
-
Cowan put short-term memory at about ▮four four chunks.
|
|
212
|
-
target: |-
|
|
213
|
-
Cowan put short-term memory at about four chunks.
|
|
214
|
-
- goal: 'Delete the doubled "familiar".'
|
|
215
|
-
start: |-
|
|
216
|
-
Rereading builds a fluency illusion: the text feels ▮familiar familiar, not known.
|
|
217
|
-
target: |-
|
|
218
|
-
Rereading builds a fluency illusion: the text feels familiar, not known.
|
|
219
181
|
- goal: 'Delete the doubled "and".'
|
|
220
182
|
start: |-
|
|
221
183
|
daw: d for delete, a for a word ▮and and its space.
|