What’s the fuss about? One can simply hover and click the mouse to navigate anywhere: among characters, words, lines, split tabs, files, or even hosts. Yes, if the mouse is available.

Mouse is indeed an important HID, and I rely on it in daily use cases.

Most coding experiences are similar to playing first-person shooter games, as they both require quick, precise mouse movements and rapid keyboard typing while scanning the screen. However, working on remote servers without an X-Windows (or Windows) system can be frustrating. This awkwardness can arise on a bare-metal cloud machine or small embedded devices.

It turns out that with substantial learning, even without a mouse, text editing with only a keyboard is enjoyable. Purging the analog input and memorizing painful short keys works. I feel my brain is actually a kind of computer, a keyboard-only solution uses more of my brain memory to do the same effect with the mouse. Think it computational, different computing paths to the same goal have different effects.

Scopes

There are hierarchical many-to-one relations.

Let's talk about moving from a smaller scope to a larger one. The basic unit of moving is a character (a grid). Roughly speaking, characters form a word, words form a line, and lines form a paragraph. A paragraph is a function, a list, a table, a class, or whatever. "Stopping here". The previously mentioned could be text objects which are customizable and operable cognitive units (the magic for keyboard-only scenarios). For more details, refer to :help text-objects. The concept compensates for the unavailability of the mouse.

Let's continue: Paragraphs form a buffer(a document in memory buffer), an active buffer lives in a window, a window lives in a tab page (the concept of a window in Vim is closer to a pane in Tmux), and tab pages live in a session.

The hierarchy and terminology in the Vim manual are here. Let's conquer the movements at each level one by one.

Character-wise

  • h, j, k, l, are the basic keys to move by a grid/character/line.
  • f N f{char} to the Nth occurrence of {char} to the right
  • F N F{char} to the Nth occurrence of {char} to the left
  • t N t{char} till before the Nth occurrence of {char} to the right
  • T N T{char} till before the Nth occurrence of {char} to the left
  • ; N ; repeat the last "f", "F", "t", or "T" N times
  • , N , repeat the last "f", "F", "t", or "T" N times in the opposite direction
  • / or ? Needless to say, the search command for any pattern. It is the "Ctrl-F" in Windows applications.

Remark

Some movements take an integer argument. Take "w N w", as an example with cursor at the "2" in 12309asdddddd, one can use 3fd to reach the 3rd "d" in the string. "N" from the manual refers to the integer argument.

Word-wise

The default word objects may not be optimized for codes since "programming languages often use either CamelCase ("anIdentifier") or underscore_notation ("an_identifier") naming conventions for identifiers."

  • w N w N words forward
  • W N W N blank-separated |WORD|s forward
  • e N e forward to the end of the Nth word
  • E N E forward to the end of the Nth blank-separated |WORD|
  • b N b N words backward
  • B N B N blank-separated |WORD|s backward
  • ge N ge backward to the end of the Nth word
  • gE N gE backward to the end of the Nth blank-separated |WORD|
  • gd if going to definition of the symbol, if LSP or diagnostic tool enabled.
  • / or ? Needless to say, the search command for any pattern

One may consider text object addons There are rich builtin text objects but for some specific languages like Java/C++/C. Not generally applicable and not listed here.

Line-wise

  • 0 0 to first character in the line (also: key)
  • ^ ^ to first non-blank character in the line
    • ^ might be hard to press, combo 0w is effectively doing the same job.
  • $ N $ to the next EOL (end of line) position (also: key)
  • gm gm to middle of the screen line
  • gM gM to middle of the line
  • | N | to column N (default: 1)
  • G N G goto line N (default: last line), on the first non-blank character
  • gg N gg goto line N (default: first line), on the first non-blank character
  • N% N % goto line N percentage down in the file; N must be given and 0-100.

Paragraph-wise

"Paragraph" is consecutive non-blank lines.

  • {, }, move around islands of consecutive non-blank lines.

Various higher-level text objects exist in different structured contexts. This might be a Java or Python function or HTML tag. Installing text object plugins or creating scripts may be necessary to expand the collection of text objects. "LSP" or language diagnostic plugins greatly augment text objects for languages. Combined with the search command, it allows almost any complicated moving tricks.

On the other hand, Vim's tagging feature enables quick global searches across projects or directories, which is invaluable. Tagging involves placing labels on different objects in structured text, such as headings in Markdown or classes in codes. The feature is very powerful, and one can tag any pattern.

Buffers

Buffer is a special concept in Vim. It is document-to-memory correspondence in a Vim session. A buffer is actually a state object in memory. A buffer for a document is only created if the document was opened previously in the session. A buffer can be open, hidden, or some other states in Vim's nomenclature. Note that buffer is attributed with "type". Check :h buftype for what values it can be.

  • :e {filepath}: load file into (new) buffer or open existing buffer. Note filepath is a document in filesystem.
  • :b {num} or :b {buffer_path}, set the target buffer to the active window.
    • Note buffer_path is some opened document in the buffer list.
    • Note one can use :ls to list buffers or use "tab" key to auto-complete partial filepath.
  • :find {filepath keyword}, handy builtins to open buffer of the file.
  • :bd {buffer_path}/{num}, buffer delete. Unload the file buffer from this session.
  • marks, mark position with registers on a buffer.
    • m m{a-zA-Z} mark current position with mark {a-zA-Z}
    • `a `{a-z} go to mark {a-z} within current file
    • `A `{A-Z} go to mark {A-Z} in any file
    • `. `. go to the position of the last change in this file, or g; for change list
    • :marks :marks print the active marks
    • CTRL-O N CTRL-O go to Nth older position in jump list
    • CTRL-I N CTRL-I go to Nth newer position in jump list
    • :ju :ju[mps] print the jump list

For more flexible operations on buffers, I would recommend the ideas like from Asehq's close-buffers

A tip

Note it is a many-to-one relationship. The same buffer can exist in multiple windows across tabpages. It is good to look at multiple positions of a document during editing. :b {buf} is a good tool. However, it is not always desired, and I would want to jump to a window of an opened buffer, but there are many opened windows and buffers. The pain is using eyes to scan through many buffer file paths. Should look for efficiency.

Consider this snippet in config:

function MoveToOpenBuf(target_buf)
  let rbufnr = bufnr(a:target_buf)
  for tp in range(1,tabpagenr('$'))
    let bfl = tabpagebuflist(tp)
    if index(bfl,rbufnr) != -1
      execute('tabnext ' .. tp)
      let bufwinid = bufwinid(a:target_buf)
      call win_gotoid(bufwinid)
      return
    endif
  endfor
  echohl WarningMsg | echo "the buffer not found" | echohl None
endfunction

command! -nargs=1 -complete=buffer B call MoveToOpenBuf(<f-args>)

The above snippet creates a command :B {buf} that jumps to the window of the targeted buffer rather than changing the buffer of the current window.

Windows

Under a tagpage, there can be windows, windows are stateful objects. In each tagpage, the number id for each windows is unique, the first window start with window id "1". One can use win_getid() API to verify.

  • M,L,H for moving cursor to middle/bottom/top of window screen
  • scrolling, like the mouse middle-button:
    • CTRL-E N CTRL-E window N lines downwards (default: 1)
    • CTRL-D N CTRL-D window N lines Downwards (default: 1/2 window)
    • CTRL-F N CTRL-F window N pages Forwards (downwards)
    • CTRL-Y N CTRL-Y window N lines upwards (default: 1)
    • CTRL-U N CTRL-U window N lines Upwards (default: 1/2 window)
    • CTRL-B N CTRL-B window N pages Backwards (upwards)
    • z<CR> z or zt redraw, current line at top of window
    • z. z. or zz redraw, current line at center of window
    • z- z- or zb redraw, current line at bottom of window
    • These only work when 'wrap' is off:
      • zh N zh scroll screen N characters to the right
      • zl N zl scroll screen N characters to the left
      • zH N zH scroll screen half a screenwidth to the right
      • zL N zL scroll screen half a screenwidth to the left
  • wincmd, windows commands, check :h wincmd
    • :terminal :terminal {cmd} open a terminal window
      • vim+tmux or vim+terminal emulator? If the terminal is not printing a lot and running stable job, vim+terminal has better integration actually.
    • CTRL-W_] CTRL-W ] split window and jump to tag under cursor
    • CTRL-W_T CTRL-W T move and sent the current window to new tabpage
    • CTRL-W_n CTRL-W n or :new create new empty window
    • CTRL-W_{hjkl} move cursor to the left/down/up/right window
    • CTRL-W_{HJKL} move current window to left/down/up/right-most side
    • CTRL-W_= CTRL-W = make all windows equal height & width
    • CTRL-W__ CTRL-W _ set current window height (default: very high)
    • CTRL-W_< CTRL-W < decrease current window width
    • CTRL-W_> CTRL-W > increase current window width
    • CTRL-W_bar CTRL-W | set current window width (default: widest possible)

It is common to see Vimers like to remap the resizing and moving to:

if has('linux')
  nnoremap <silent> <A-h> <C-w>h
  nnoremap <silent> <A-j> <C-w>j
  nnoremap <silent> <A-k> <C-w>k
  nnoremap <silent> <A-l> <C-w>l
elseif has('macunix')
  " optional for macOS, note macOS uses alt for special symbol input
  nnoremap <silent> ˙ <C-w>h
  nnoremap <silent><C-w>j
  nnoremap <silent> ˚ <C-w>k
  nnoremap <silent> ¬ <C-w>l
endif

" +/- split windows size
map + <C-W>5>
map - <C-W>5<
map <PageDown> <C-w>5-
map <PageUp> <C-w>5+
map zh 10z<Left>
map zl 10z<Right>

nnoremap <silent> <C-y> 5<C-y>
nnoremap <silent> <C-e> 5<C-e>

Current working directory

Note each window is aware of working directory for :e, :w, :r or whatever Vim commands. Use :cd {path} to switch the global working directory and :lcd {path} for the current window.

Tabpages

(still working, if one wants this, leave a comment)

Sessions

(still working, if one wants this, leave a comment)

Conclusion

Most explanations of the above motions can be found at :help quickref.

Congratulations on reaching this point. The simple actions commonly used by many modern editors have to be re-learned in this manner. Practice is the key to building up muscle memory, which is irreplaceable!

Your thoughts...