% vim-editing-grammar.tex — Vim as a language: modes, operator + motion/text-object grammar,
% motions, text objects, registers, macros, marks + jumps, search/replace + filters,
% buffers/windows/tabs, undo tree, config (vim-plug, Neovim built-in LSP), a fixed
% rename-file function.
% Source: docs/old-notes/utils/vim.md
% Checked 2026-09-25 against Vim's runtime/doc (change.txt: registers, !!, :s flags, & g&,
% J gJ, g?, CTRL-A; repeat.txt: :g :v, q, @, @@, @:; motion.txt: text objects, marks,
% jumplist, changelist, operators + doubling; visual.txt: gn, block I/A, gv, o; pattern.txt:
% \v, \< \>, \zs \ze, * # g*, empty pattern; undo.txt: g- g+ :earlier; editing.txt: ZZ = :x
% vs :wq, :saveas; windows.txt: :sp/:vs, buffer/window/tab; terminal.txt: CTRL-\ CTRL-N)
% and Neovim runtime/doc/lsp.txt (vim.lsp.config/enable, grn gra grr).
% Source errors fixed here: ":sp = vertically, :vs = horizontally" is swapped (:sp stacks
% windows = horizontal split, :vs puts them side by side = vertical); the RenameFile function
% used an undefined old_file and shelled out to rm (breaks on spaces) — rewritten with
% delete() + :bwipeout #; ":eariler" typo; ZZ is :x (writes only if modified), not exactly :wq.
% Build ONLY with: tools/print/print-sheet.py <this>.tex --dry-run
% @source: hiot monorepo, docs/school/sheets/dev-tools/vim-editing-grammar.tex — the SOURCE OF TRUTH; a copy anywhere else (e.g. artur.gurgul.pro) is regenerated from it, never edited
% @labels: area=dev-tools kind=tooling level=core platform=general new=no round=website-notes-2026-09-25 topic=tooling
% @tags: vim, neovim, modal-editing, motions, text-objects, operators, registers, macros, marks, search-replace, splits, vimrc
\documentclass[8pt]{extarticle}
\usepackage{printup-sheet}
\usepackage{array}

\lstdefinelanguage{VimSheet}{
  morekeywords={function,endfunction,let,if,endif,return,execute,call,nnoremap,silent,abort},
  sensitive=true, morecomment=[l]{"}, morestring=[b]'}
\lstdefinestyle{vim}{style=printup, language=VimSheet,
  literate={==}{{\hbox{=}\hbox{=}}}2 {->}{{\hbox{-}\hbox{>}}}2 {||}{{\hbox{|}\hbox{|}}}2}

\tikzset{
  sb/.style={box, font=\scriptsize, inner sep=1.5pt, minimum height=4.6mm},
  md/.style={sb, minimum width=15mm},
  lbl/.style={font=\tiny, text=black!75, inner sep=1pt, align=center},
  pt/.style={font=\bfseries\small, anchor=west},
  seg/.style={font=\ttfamily\small, inner sep=0pt, anchor=west, minimum height=4mm},
  gr/.style={sb, minimum width=15mm, minimum height=6mm},
  br/.style={decorate, thick},
}
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}
\newcommand\bq{\textasciigrave}
\newcommand\ca{\textasciicircum}
\newcommand\bs{\textbackslash}
\newcommand\gtgt{\hbox{>}\hbox{>}}
\newcommand\ltlt{\hbox{<}\hbox{<}}
\newcommand\bang{\hbox{!}\hbox{!}}
\newcommand\sls{\hbox{/}\hbox{/}}

\begin{document}

\sheettitle{Vim — the editing language}{dev-tools · memo}

\oneliner{Vim is \textbf{modal}: Normal mode is a \textbf{language} —
\texttt{[count] operator [count] motion|text-object} (\texttt{d2w}, \texttt{ci"}, \texttt{gUip});
an operator typed twice acts on the line (\texttt{dd}, \texttt{\gtgt}), and \texttt{.} repeats
the last change. Insert is only for typing, Visual selects then operates, \texttt{:} runs Ex
commands over line ranges.}

\vspace{2pt}
\noindent\begin{tikzpicture}[sheet]
  % ── 1 modes ──
  \node[pt] at (-0.1,3.1) {\textcolor{sheetBlue}{1} modes};
  \node[md, draw=sheetBlue, very thick, fill=sheetBlue!15, minimum width=13mm, minimum height=27mm] (N) at (0.6,1.3) {\textbf{Normal}\\(home)};
  \foreach \y/\n/\t/\c in {2.5/I/Insert/sheetGreen!70!black, 1.7/V/Visual/sheetOrange, 0.9/C/Cmdline/sheetGrey, 0.1/T/Terminal/sheetGrey}
    \node[md, draw=\c, fill=\c!10, minimum height=5mm] (\n) at (4.75,\y) {\t};
  \foreach \n/\go/\back in {I/{\texttt{i a I A o O s c}}/{\texttt{Esc}},
                            V/{\texttt{v V Ctrl-v}}/{\texttt{Esc} or an operator},
                            C/{\texttt{: / ?}}/{\texttt{Enter} / \texttt{Esc}},
                            T/{\texttt{:term}, then \texttt{i}}/{\texttt{Ctrl-\bs{} Ctrl-N}}} {
    \draw[flow] ($(N.east |- \n.west)+(0,0.07)$) -- node[lbl, above]{\go} ([yshift=0.7mm]\n.west);
    \draw[flow, sheetRed] ([yshift=-0.7mm]\n.west) -- node[lbl, below, text=sheetRed]{\back} ($(N.east |- \n.west)+(0,-0.07)$);
  }
  % ── 2 grammar ──
  \draw[sheetGrey!40] (5.95,3.2) -- (5.95,-0.35);
  \node[pt] at (6.0,3.1) {\textcolor{sheetBlue}{2} the grammar: verb + noun};
  \node[gr, fill=black!4, draw=sheetGrey] (g1) at (6.85,2.35) {count};
  \node[gr, draw=sheetRed, fill=sheetRed!7] (g2) at (8.55,2.35) {operator};
  \node[gr, fill=black!4, draw=sheetGrey] (g3) at (10.25,2.35) {count};
  \node[gr, draw=sheetGreen!70!black, fill=sheetGreen!10, minimum width=18mm] (g4) at (12.1,2.35) {motion | text obj};
  \foreach \a/\b in {g1/g2,g2/g3,g3/g4} \draw[flow] (\a) -- (\b);
  \node[lbl, anchor=west, align=left, font=\scriptsize] at (6.0,1.55) {\texttt{d2w} delete 2 words\qquad\texttt{3dd} delete 3 lines};
  \node[lbl, anchor=west, align=left, font=\scriptsize] at (6.0,1.15) {\texttt{ci"} change inside quotes\qquad\texttt{yap} yank a paragraph};
  \node[lbl, anchor=west, align=left, font=\scriptsize] at (6.0,0.75) {\texttt{gUiw} upper-case word\qquad\texttt{=G} re-indent to end};
  \node[lbl, anchor=west, align=left, font=\scriptsize] at (6.0,0.35) {\texttt{dfx} delete through next \texttt{x}\qquad\texttt{\gtgt} indent line};
  \node[lbl, anchor=west, align=left, text=sheetBrown] at (6.0,-0.1) {then \texttt{.} repeats the whole change; \texttt{j.} \texttt{j.} \ldots};
  % ── 3 text objects ──
  \draw[sheetGrey!40] (13.3,3.2) -- (13.3,-0.35);
  \node[pt] at (13.35,3.1) {\textcolor{sheetBlue}{3} text objects};
  \node[seg] (s1) at (13.55,1.75) {f};
  \node[seg] (s2) at (s1.east) {(};
  \node[seg] (s3) at (s2.east) {"};
  \node[seg, text=sheetOrange] (s4) at (s3.east) {big};
  \node[seg] (s5) at (s4.east) {\ deal};
  \node[seg] (s6) at (s5.east) {"};
  \node[seg] (s7) at (s6.east) {, x};
  \node[seg] (s8) at (s7.east) {)};
  \draw[br, decoration={brace, amplitude=2pt}, sheetBlue] ([yshift=1.5mm]s3.north west) -- node[lbl, above=2pt]{\texttt{i(}} ([yshift=1.5mm]s7.north east);
  \draw[br, decoration={brace, amplitude=2pt}, sheetBlue] ([yshift=6.5mm]s2.north west) -- node[lbl, above=2pt]{\texttt{a(} = with the parens} ([yshift=6.5mm]s8.north east);
  \draw[br, decoration={brace, mirror, amplitude=2pt}, sheetOrange] ([yshift=-1.5mm]s4.south west) -- node[lbl, below=2pt]{\texttt{iw}} ([yshift=-1.5mm]s4.south east);
  \draw[br, decoration={brace, mirror, amplitude=2pt}, sheetGreen!60!black] ([yshift=-6.5mm]s4.south west) -- node[lbl, below=2pt]{\texttt{i"}} ([yshift=-6.5mm]s5.south east);
  \draw[br, decoration={brace, mirror, amplitude=2pt}, sheetGreen!60!black] ([yshift=-11.5mm]s3.south west) -- node[lbl, below=2pt]{\texttt{a"}} ([yshift=-11.5mm]s6.south east);
  \node[lbl, anchor=west, align=left] at (13.35,-0.3) {cursor on \textcolor{sheetOrange}{big} · \texttt{i} inner,\\\texttt{a} around (+ delimiters / space)};
\end{tikzpicture}

\begin{multicols}{2}

\section{How it works}
\begin{itemize}\raggedright
  \item Operators: \texttt{d} delete · \texttt{c} change · \texttt{y} yank · \texttt{> <}
        shift · \texttt{=} indent · \texttt{gu gU g\textasciitilde} case · \texttt{gq} format ·
        \texttt{g?} rot13 · \texttt{!} filter through a program · \texttt{zf} fold. Doubled =
        the line (\texttt{dd}, \texttt{gUU}); \texttt{D}~=~\texttt{d\$}, \texttt{C}~=~\texttt{c\$}.
  \item Text objects (after an operator or in Visual): \texttt{iw aw} · \texttt{iW} ·
        \texttt{is ip} · \texttt{i( a(} (= \texttt{ib}) · \texttt{i\{} (= \texttt{iB}) ·
        \texttt{i[} · \texttt{i<} · \texttt{it at} (tags) · \texttt{i" i' i\bq}.
  \item \texttt{.} repeats the last \emph{change}. Undo is a \textbf{tree}: \texttt{u},
        \texttt{Ctrl-R}, \texttt{U} line, \texttt{g- g+}, \texttt{:earlier 10m}, \texttt{'undofile'}.
  \item Insert: \texttt{i a} before/after cursor · \texttt{I A} line start/end ·
        \texttt{o O} new line below/above. Visual: \texttt{o} other end, \texttt{gv}
        reselect, block \texttt{I}/\texttt{A} text \texttt{Esc} edits \emph{every} line,
        \texttt{gn} selects the next match.
\end{itemize}

\section{Motions}
{\scriptsize\setlength{\tabcolsep}{2pt}
\begin{tabular}{@{}>{\ttfamily}l L{23mm} >{\ttfamily}l L{21mm}@{}}
w b e ge & word fwd/back/end (\texttt{W}: WORD) & gg G 50G & top · end · line 50 \\
0 \ca{} \$ g\_ & start · 1st char · end · last char & \{ \} ( ) & paragraph · sentence \\
fx tx ; , & to/till \texttt{x}, repeat, reverse & \% & matching bracket \\
{}* \# g* & word under cursor fwd/back & H M L & screen top/mid/bottom \\
/p ?p n N & search, next/prev match & Ctrl-D/U & half page (\texttt{F/B}: full) \\
Ctrl-O/I & jumplist back/fwd & zz zt zb & line to mid/top/bottom \\
\end{tabular}}

\section{Search, replace, filters}
\begin{itemize}\raggedright
  \item \texttt{:\%s/foo/bar/gc}: \texttt{g} every match (default: first per line),
        \texttt{c} confirm, \texttt{i}/\texttt{I} case, \texttt{n} count. Ranges \texttt{\% . \$
        '<,'> 10,20 .,+3}.
  \item Empty pattern = last search (\texttt{*} then \texttt{:\%s\sls{}bar/g}); \texttt{\bs v}
        very magic, \texttt{\bs<w\bs>} word, \texttt{\bs zs}/\texttt{\bs ze}; \texttt{\&},
        \texttt{g\&} repeat \texttt{:s}. \texttt{:g/pat/d} deletes matches, \texttt{:v/pat/d}
        keeps them.
  \item Filters: \texttt{\bang{}date} = \texttt{:.!date} · \texttt{:\%!sort} ·
        \texttt{:r !date} inserts output · \texttt{:so \%}.
\end{itemize}

\section{Example — rename the current file (fixed)}
\begin{lstlisting}[style=vim]
function! RenameFile() abort
  let oldname = expand('%')
  let newname = input('New name: ', oldname, 'file')
  if newname ==# '' || newname ==# oldname | return | endif
  execute 'saveas' fnameescape(newname) " old -> alternate #
  call delete(oldname)     " no shell: '!rm' breaks on spaces
  silent! bwipeout #       " the note used undefined old_file
endfunction
nnoremap <leader>n :call RenameFile()<CR>
\end{lstlisting}

\section{Config}
\raggedright Vim \texttt{\textasciitilde/.vimrc} · Neovim
\texttt{\textasciitilde/.config/nvim/init.\{vim,lua\}}: \texttt{set number expandtab
shiftwidth=2}, \texttt{filetype plugin indent on}, \texttt{nnoremap} (non-recursive map).
vim-plug: \texttt{Plug 'junegunn/fzf'} between \texttt{plug\#begin()}/\texttt{end()}. Neovim
LSP is built in: \texttt{vim.lsp.enable('sourcekit')} $\to$ \texttt{grn} rename, \texttt{gra}
action, \texttt{grr} refs.


\section{Registers}
{\scriptsize\setlength{\tabcolsep}{2pt}
\begin{tabular}{@{}>{\ttfamily}l L{67mm}@{}}
"{}" & unnamed: every \texttt{d c s x y} lands here; \texttt{p} reads it \\
"0 & last \textbf{yank} — survives later deletes \\
"1..."9 & deletes of a line or more, shifting down; \texttt{"-} small (< 1 line) delete \\
"a..."z & named; \textbf{\texttt{"A}} (upper case) \textbf{appends} \\
"\_ & black hole: delete without clobbering anything \\
"+ "* & system clipboard / primary selection \\
". ": "\% "\# & last insert · last Ex cmd · file name · alternate file (read-only) \\
"/ "= & last search · expression (\texttt{Ctrl-R =5*5} inserts 25) \\
\end{tabular}}\\[1pt]
\texttt{"ayiw} · \texttt{"ap} · \texttt{Ctrl-R a} in Insert/cmdline · \texttt{:reg}.

\section{Macros — a macro is a register}
\texttt{qa}\ldots\texttt{q} record · \texttt{@a} play · \texttt{5@a} · \texttt{@@} last macro
· \texttt{@:} last Ex command. \textbf{Edit}: \texttt{"ap}, fix the text,
\texttt{0"ay\$}; or \texttt{:let @a='\ldots'}; \texttt{qA} appends. \textbf{Apply}:
\texttt{:\%normal @a} · \texttt{:'<,'>normal @a} · \texttt{:10,20normal @a} ·
\texttt{:g/pat/normal @a}. Start at \texttt{0}, end with \texttt{j}: \texttt{100@a} chains.

\section{Marks and jumps}
\texttt{ma} set · \texttt{\bq a} exact spot · \texttt{'a} its line · \texttt{a}–\texttt{z} per file,
\texttt{A}–\texttt{Z} global (jump to the file) · \texttt{\bq\bq} before the last jump ·
\texttt{\bq.} last change · \texttt{\bq"} where you left the buffer · \texttt{\bq[ \bq]}
last changed/yanked text · \texttt{g; g,} change list · \texttt{:marks}.

\section{Buffers, windows, tabs}
\textbf{Buffer} = a file's text in memory; \textbf{window} = a viewport on one;
\textbf{tab} = a set of windows. \texttt{:ls} · \texttt{:b2} · \texttt{:bd} ·
\texttt{:e \#}/\texttt{Ctrl-\ca} alternate · \texttt{:sp} = \textbf{horizontal} split
(stacked) · \texttt{:vs} = \textbf{vertical} (side by side) ·
\texttt{splitbelow}/\texttt{splitright} flip · \texttt{Ctrl-W s v w hjkl o = q} ·
\texttt{:tabe}, \texttt{gt gT} · \texttt{:term} opens a split.

\section{Interview traps}
\begin{itemize}\raggedright
  \trap{\texttt{:sp} is the \emph{horizontal} split — the note had the two swapped.}
  \trap{\texttt{ZZ} = \texttt{:x}: writes only if modified; \texttt{:wq} always writes.
        \texttt{ZQ} = \texttt{:q!}.}
  \trap{A delete overwrites \texttt{""}: paste the earlier yank with \texttt{"0p}, or
        delete into \texttt{"\_}.}
\end{itemize}

\section{Remember}
\textbf{Verb + noun, then dot.} A macro is a register.

\section{Likely questions}
\begin{enumerate}\raggedright
  \item Same edit on 200 lines? — record \texttt{qa}\ldots\texttt{q}, then \texttt{:\%normal @a}.
  \item Replace a word with the yank, twice? — \texttt{ciw} \texttt{Ctrl-R 0}, then \texttt{.}.
  \item Keep only lines matching \texttt{err}? — \texttt{:v/err/d}.
\end{enumerate}

\end{multicols}

\noindent{\footnotesize\color{sheetGrey}\textit{Related:} shell filters (\texttt{sort},
\texttt{column}) · regular expressions · LSP · tmux · git from the editor}

\end{document}
