Platform & Tooling · پلتفرم و ابزار پایهBeginner ~60 دقیقه مطالعه~50 min read

vim: از بقا تا تسلطvim: From Survival to Mastery

راهنمای کامل vim از صفر تا سطح سنیور: ایده‌ی modal، گرامر operator + motion، جستجو و substitute و :global، register و macro و mark، مدیریت buffer و window و netrw، یک .vimrc خط‌به‌خط، vim داخل git، نکات امنیتی production و یک برگه‌ی تقلب عملی.A complete zero-to-senior guide to vim: the modal idea, the operator + motion grammar, search and substitute and :global, registers, macros and marks, buffer/window/netrw management, a line-by-line .vimrc, vim inside git, production security pitfalls, and a practical cheat sheet.


یک روز SSH می‌زنی به یک سرور production تا یک خط از یک فایل config را عوض کنی و می‌بینی روی آن ماشین نه IDE هست، نه رابط گرافیکی — فقط یک ترمینال و ادیتوری که دهه‌هاست روی هر سیستم یونیکسی نصب است: vim.

یا ساده‌تر: git commit می‌زنی بدون -m، صفحه عوض می‌شود، هرچه تایپ می‌کنی اتفاق عجیبی می‌افتد و Ctrl-C هم نجاتت نمی‌دهد. این همان لحظه‌ی معروف «گیر افتادن در vim» است.

این فصل از صفر شروع می‌کند — فرض این است که حتی بلد نیستی از vim خارج شوی — و تا جایی می‌رود که یک refactor تکراری را با یک macro در ده ثانیه انجام دهی، در وسط یک git rebase -i تصمیم درست بگیری، و در مصاحبه جواب سنیور بدهی.

نقشه‌ی راه این فصل

بقا (۱–۴): چرا vim مهم است، کارت خروج اضطراری، ایده‌ی modal، باز کردن و ذخیره و بستن. زبانِ ویرایش (۵–۸): motionها، گرامر operator + motion، undo و نقطه، ورود هوشمند به insert. قدرت واقعی (۹–۱۴): جستجو و :s و :g، visual block، registerها، macroها، markها، مدیریت buffer و window و tab و netrw. سطح سنیور (۱۵–۲۱): یک .vimrc خط‌به‌خط و بحث XDG در Vim 9.2، پلاگین‌ها با قضاوت، vim داخل git، امنیت و بهداشت production، انتخاب بین vim و nano، تمرین‌ها و برگه‌ی تقلب.


۱. چرا vim هنوز مهم است

کسی از تو نمی‌خواهد کل کد Spring Boot را در vim بنویسی؛ برای آن IDE داری. اما چهار موقعیت هست که vim در آن‌ها انتخاب نیست، واقعیت است:

  • سرورها و containerها. تقریباً هر توزیع لینوکسی vim یا vi دارد. داخل یک pod یا VM، رابط گرافیکی وجود ندارد و application.yml یا nginx.conf را باید همان‌جا عوض کنی.
  • git. git commit، git rebase -i، git tag -a و git add -p ادیتور باز می‌کنند و آن ادیتور اغلب vim است.
  • ابزارهای cloud-native. kubectl edit deployment/api یک YAML موقت را در ادیتورت باز می‌کند و بعد از ذخیره به API server می‌فرستد. crontab -e و visudo هم همین‌طور. اگر ادیتور را بلد نباشی، عملاً آن دستور را بلد نیستی.
  • سرعت روی متن. وقتی متن را به‌عنوان ساختار می‌بینی (کلمه، بلوک، پاراگراف)، ویرایشی که جای دیگر سی ثانیه است، دو ثانیه می‌شود.
vim مثل یک آچار در جیب است

IDE مثل یک کارگاه کامل است: پرس، جرثقیل، دستگاه CNC. عالی است — تا وقتی کنار جاده لاستیک پنچر کنی. آنجا چیزی به کارت می‌آید که همیشه در جیبت بوده. vim آن آچار است: کوچک، همه‌جا حاضر، و اگر بلد باشی، از هر جای گیری بیرونت می‌آورد.

نسخه‌ای که مبنای این فصل است

Vim 9.2 (منتشرشده ۱۴ فوریه ۲۰۲۶). با vim --version نسخه‌ات را ببین. تقریباً همه‌ی چیزی که اینجا می‌آموزی از Vim 7 به بعد کار می‌کند؛ هرجا قابلیتی نسخه‌ی خاصی می‌خواهد، صریحاً گفته می‌شود.


۲. نجات اضطراری: چطور از vim بیرون بیایم

قبل از هر آموزشی، کارت خروج:

1) کلید Esc را بزن  (چند بار هم ضرر ندارد)
2) تایپ کن:  :q!    و Enter   → خروج بدون ذخیره
   یا تایپ کن:  :wq  و Enter   → ذخیره و خروج

چرا اول Esc؟ چون احتمالاً در insert mode بوده‌ای و کلیدهایت به متن اضافه می‌شده، نه به ادیتور فرمان می‌داده.

  • E37: No write since last change — تغییر ذخیره‌نشده داری: :q! (دور بریز) یا :wq (ذخیره کن).
  • چند فایل باز است:qa! همه را می‌بندد.
  • صفحه قفل شده — احتمالاً Ctrl-S ترمینال است، نه vim؛ با Ctrl-Q باز می‌شود.
  • vim ناپدید شدCtrl-Z آن را suspend کرده، نکشته؛ در شل fg بزن.
پیام ATTENTION و فایل swap

اگر موقع باز کردن فایل پیام E325: ATTENTION ... swap file ... already exists! گرفتی، یعنی یا یک vim دیگر همان فایل را باز دارد، یا یک session قبلی crash کرده و .filename.swp باقی مانده.

گزینه‌ی امن R (Recover) است: نسخه‌ی بازیابی‌شده را ببین، مقایسه کن، بعد فایل swap را پاک کن. هرگز کورکورانه D نزن، چون ممکن است تنها نسخه‌ی تغییرات نجات‌نیافته را نابود کنی. روی سرور مشترک هم اول مطمئن شو کسی همان لحظه فایل را باز ندارد؛ دو نفر که هم‌زمان یک nginx.conf را ویرایش کنند، آخری تغییرات اولی را پاک می‌کند.

«در vim گیر افتادی؛ دقیقاً چه می‌کنی؟»

سؤال گرم‌کردنی است، اما جواب نشان می‌دهد چقدر با ترمینال راحتی: «اول Esc تا مطمئن شوم در normal mode هستم. برای دور ریختن :q!، برای ذخیره :wq یا ZZ، و اگر چند buffer باز است :qa!. اگر ادیتور اصلاً جواب نمی‌دهد، احتمالاً ترمینال با Ctrl-S قفل شده که Ctrl-Q بازش می‌کند، یا vim با Ctrl-Z معلق شده که با fg برمی‌گردد.» جمله‌ی آخر تفاوت «حفظ کرده» و «می‌فهمد» را نشان می‌دهد.


۳. ایده‌ی modal: مهم‌ترین مفهوم کل فصل

در یک ادیتور معمولی، صفحه‌کلید همیشه یک کار می‌کند: حرف تولید می‌کند. برای هر کار دیگر باید سراغ modifier یا ماوس یا منو بروی. یعنی ۲۶ حرف الفبا فقط برای تایپ «هدر می‌روند».

vim معامله‌ی دیگری می‌کند: حالت‌ها (modes). در حالت پیش‌فرض (normal mode)، d یعنی delete، y یعنی yank (کپی)، p یعنی paste. برای تایپ کردن باید صریحاً وارد insert mode شوی.

جعبه‌دنده‌ی ماشین

همان پدال گاز، در دنده‌ی یک یعنی «آرام و پرقدرت» و در دنده‌ی پنج یعنی «سرعت زیاد». پدال عوض نشده؛ حالت عوض شده.

کلید d هم عوض نمی‌شود؛ حالت عوض می‌شود. تازه‌کارها ناراحتند چون فکر می‌کنند همیشه باید در دنده‌ی تایپ باشند. حرفه‌ای‌ها می‌دانند حالت پیش‌فرض، دنده‌ی فرمان‌دهی است — چون برنامه‌نویس بیشتر وقتش را صرف خواندن و جابه‌جا کردن کد می‌کند تا تایپ کد جدید.

حالت چطور واردش شوی چه می‌کند چطور بیرون بیایی
Normal پیش‌فرض؛ Esc حرکت، حذف، کپی، paste، فرمان
Insert i a o I A O c تایپ متن Esc یا Ctrl-[
Visual v کاراکتری · V خطی · Ctrl-v بلوکی انتخاب، سپس عملیات Esc
Command-line : فرمان Ex · / ? جستجو :w، :s، :e Enter یا Esc

نمودار: ماشین حالت modeها در vim — The vim mode state machine.

stateDiagram-v2
    [*] --> Normal
    Normal --> Insert: i a o I A O c
    Insert --> Normal: Esc
    Normal --> Visual: v V Ctrl-v
    Visual --> Normal: Esc
    Visual --> Insert: c s
    Normal --> CommandLine: colon slash question
    CommandLine --> Normal: Enter or Esc
    Normal --> Replace: R
    Replace --> Normal: Esc
قانون طلایی تازه‌کارها

در insert mode زندگی نکن. اشتباه رایج این است که آدم i می‌زند و تا آخر جلسه آنجا می‌ماند و با کلیدهای جهت‌نما حرکت می‌کند؛ یعنی از vim به‌عنوان یک Notepad ضعیف استفاده می‌کند.

عادت درست: یک فکر = یک ورود و خروج از insert. حالت استراحتِ تو normal است. نشانه‌ی پیشرفت این است که انگشتت بعد از هر جمله خودکار Esc بزند.

Esc دور است — سه جایگزین. Ctrl-[ دقیقاً معادل Esc است و همه‌جا کار می‌کند. Ctrl-c تقریباً معادل است اما InsertLeave و abbreviation را اجرا نمی‌کند. راه سوم map کردن است (inoremap jk <Esc>)، اما روی ماشین دیگری وجود ندارد — پس Ctrl-[ را یاد بگیر.


۴. باز کردن، ذخیره، خروج

vim app.yml            # باز کردن یک فایل (اگر نباشد، موقع ذخیره ساخته می‌شود)
vim +42 app.yml        # باز کردن و پریدن به خط ۴۲
vim +/timeout app.yml  # پریدن به اولین تطبیق «timeout»
vim -R app.yml         # باز کردن read-only
vim -d old.yml new.yml # حالت diff بین دو فایل
vim -u NONE -N app.yml # بدون vimrc و پلاگین، اما با رفتار مدرن
vim -O a.py b.py       # دو فایل کنار هم (-o برای split افقی)
فرمان معنی
:w ذخیره کن، باز بمان
:w newname ذخیره با نام دیگر (buffer روی فایل قبلی می‌ماند)
:sav newname save-as واقعی — buffer به فایل جدید متصل می‌شود
:q / :q! ببند / ببند و تغییرات را دور بریز
:wq ذخیره کن و ببند (همیشه می‌نویسد)
:x ذخیره کن و ببند — فقط اگر تغییری بوده
ZZ / ZQ معادل :x / :q! بدون دو‌نقطه
:wa :qa :wqa همان‌ها روی همه‌ی bufferها
:e! فایل را از دیسک دوباره بخوان و تغییرات محلی را دور بریز
چرا سنیورها `:x` را به `:wq` ترجیح می‌دهند

:wq همیشه می‌نویسد، حتی اگر یک کاراکتر هم عوض نکرده باشی — یعنی mtime فایل عوض می‌شود. روی ماشین خودت مهم نیست، اما روی سرور می‌تواند یک build مبتنی بر mtime را بی‌دلیل راه بیندازد، یک file-watcher را trigger کند، یا در audit log ظاهر شود و کسی فکر کند config را دست‌کاری کرده‌ای. :x و ZZ فقط وقتی می‌نویسند که واقعاً تغییری باشد.

اگر با کاربر عادی فایلی متعلق به root را ویرایش کنی، همه‌چیز تا لحظه‌ی :w خوب پیش می‌رود و بعد خطای permission می‌گیری. کارت را از دست نداده‌ای؛ buffer هنوز در حافظه است و می‌توانی با :sav /tmp/backup.conf نجاتش دهی. راه درست‌تر از اول sudoedit است — در بخش امنیت می‌بینی.


۵. Motionها: حرکت بدون ماوس

motion یعنی فرمانی که مکان‌نما را جابه‌جا می‌کند. دو دلیل برای اهمیتشان: پیمایش، و — مهم‌تر — اینکه ورودی operatorها هستند.

h چپ    j پایین    k بالا    l راست

حرکت بر اساس کلمه:

w   ابتدای کلمه‌ی بعدی        b   ابتدای کلمه‌ی قبلی
e   انتهای کلمه               ge  انتهای کلمه‌ی قبلی
W B E   همان‌ها اما «کلمه» = هر چیزی بین فاصله‌ها

روی رشته‌ی user.profile.email، کلید w نقطه‌ها را جداکننده می‌بیند و سه بار توقف می‌کند، اما W کل آن را یک کلمه می‌بیند. این تفاوت روی مسیر فایل، URL و نام package جاوا حیاتی است.

داخل خط:

0   ستون صفر        ^   اولین کاراکتر غیرفاصله
$   انتهای خط       g_  آخرین کاراکتر غیرفاصله      40| ستون ۴۰

در فایل:

gg  خط اول            G     خط آخر           42G یا :42  خط ۴۲
H M L   بالای / وسط / پایین صفحه
Ctrl-d نصف صفحه پایین   Ctrl-u نصف صفحه بالا
Ctrl-f یک صفحه جلو      Ctrl-b یک صفحه عقب
zz  خط فعلی را وسط صفحه بیاور   (zt بالا، zb پایین)
{   پاراگراف قبلی      }   پاراگراف بعدی      ( ) جمله
%   پرش به پرانتز/آکولاد/براکت متناظر

% را دست‌کم نگیر: برای پیدا کردن اینکه یک بلوک کجا بسته می‌شود — در YAML، JSON یا کد تودرتو — بی‌نظیر است.

هدف‌گیری با f t ; ,:

fx  برو جلو تا اولین x        Fx  برو عقب تا اولین x
tx  برو جلو تا درست قبل از x  Tx  برو عقب تا درست بعد از x
;   همان جستجو را تکرار کن    ,   در جهت مخالف تکرار کن

روی خط repository.findByStatusAndCreatedAfter(status, cutoff); اگر مکان‌نما ابتدای خط باشد، f( تو را مستقیم روی پرانتز باز می‌گذارد — دقیق‌تر و سریع‌تر از بیست بار l.

count. هر motion را می‌شود ضرب کرد: 3w، 5j، 2}. برای پرش عمودی set relativenumber را روشن کن تا فاصله‌ی هر خط تا مکان‌نما کنارش نوشته شود و 7k زدن حدس نباشد. اما اگر دیدی داری خط‌ها را با چشم می‌شماری تا 13j بزنی، ابزار اشتباه انتخاب کرده‌ای: برای فاصله‌های بلند، جستجو (/pattern) یا :42 یا mark تقریباً همیشه بهتر از count است.


۶. گرامر vim: جایی که همه‌چیز کلیک می‌کند

اینجا vim از «ادیتوری با کلیدهای عجیب» تبدیل می‌شود به «یک زبان کوچک برای ویرایش متن»:

[count] operator [count] {motion | text-object}

یعنی فعل + مفعول. operator فعل است (حذف کن، تغییر بده، کپی کن) و motion یا text-object مفعول.

نمودار: ساختار یک فرمان ویرایشی — Anatomy of a vim edit command.

flowchart LR
    A[count: optional number] --> B[operator: d c y gU gq]
    B --> C[motion or text object: w e dollar ap iw]
    C --> D[operation applied to that exact range]
    B -.doubled operator.-> E[dd yy cc gUU]
    E --> D
operator معنی مثال
d delete (به register هم می‌رود، پس مثل cut است) dw d$ dap
c change (حذف کن و وارد insert شو) cw ci" cc
y yank (کپی) yy yi( y$
> / < تورفتگی به راست / چپ >ap >j
= فرمت خودکار تورفتگی =ap gg=G
gu / gU / g~ کوچک / بزرگ / معکوس کردن حروف gUiw
gq شکستن متن به عرض textwidth gqap
! عبور دادن متن از یک فرمان شل !apsort

text-object ایده‌ی قوی‌تری از motion است: به‌جای «از اینجا تا آنجا»، می‌گویی «آن چیزی که در آن هستم».

iw / aw   کلمه (aw فاصله‌ی بعدش را هم می‌گیرد)
is / as   جمله          ip / ap   پاراگراف
i( a(  یا  ib ab   پرانتز        i{ a{  یا  iB aB   آکولاد
i[ a[   براکت      i" a"  و  i' a'   کوتیشن
it / at   داخل تگ HTML/XML  /  با خود تگ

حالا این‌ها را بخوان — دقیقاً همان‌طور که در ذهنت هستند تایپ می‌شوند:

ciw   کلمه‌ای که رویش هستی را عوض کن
ci"   محتوای داخل کوتیشن را عوض کن
da(   کل پرانتز و محتوایش را حذف کن
yi{   داخل آکولاد را کپی کن
dap   کل پاراگراف را حذف کن
>ip   پاراگراف را یک سطح تو ببر
gUiw  کلمه را بزرگ کن
فرمول را حفظ نکن، بساز

تو ۲۰۰ میان‌بر حفظ نمی‌کنی. حدود ۸ فعل و ۲۰ مفعول یاد می‌گیری و ضربشان ۱۶۰ فرمان می‌شود که هیچ‌کدام را جداگانه حفظ نکرده‌ای. هر operator جدیدی که بعداً یاد بگیری، فوراً با تمام مفعول‌های بلدشده ترکیب می‌شود.

این دقیقاً همان اصلی است که در طراحی API خوب هم دنبالش هستیم: مجموعه‌ای کوچک از عملیات متعامد، به‌جای انبوهی از فرمان‌های تک‌منظوره.

میان‌بر معادل معنی
dd / yy حذف / کپی کل خط
cc (یا S) تغییر محتوای خط با حفظ تورفتگی
D / C d$ / c$ حذف / تغییر تا انتهای خط
x / X dl / dh حذف کاراکتر جلو / عقب
s cl تعویض کاراکتر و ورود به insert
p / P paste بعد از / قبل از مکان‌نما
J / gJ چسباندن خط بعدی با / بدون فاصله
r{char} جایگزینی یک کاراکتر بدون ورود به insert
Ctrl-a / Ctrl-x زیاد / کم کردن عدد زیر مکان‌نما

Ctrl-a گنج پنهان است: مکان‌نما را روی یک عدد بگذار و 5Ctrl-a بزن تا پنج‌تا اضافه شود — برای بالا بردن version در pom.xml یا عوض کردن پورت در docker-compose عالی است. فقط بدان vim قدیمی عددهای شروع‌شده با 0 را octal حساب می‌کند؛ defaults.vim مدرن با set nrformats-=octal این را خاموش می‌کند و اگر vimrc خودت را نوشته‌ای باید خودت اضافه‌اش کنی.

اگر چیزی که کپی کرده‌ای «خطی» باشد (با yy یا ddp آن را در خط بعد و P در خط قبل می‌گذارد؛ vim نوع محتوای register را به‌خاطر می‌سپارد.

تله‌ی کلاسیک: حذف کردن، حافظه‌ی کپی را خراب می‌کند

با yy یک خط کپی می‌کنی، بعد با dd خط دیگری را حذف می‌کنی تا جا باز شود، بعد p می‌زنی — و به‌جای خط کپی‌شده، همان خط حذف‌شده paste می‌شود.

دلیل: d هم مثل y در register بی‌نام ("") می‌نویسد و آخرین نوشتن برنده است. دو راه‌حل: "0p که همیشه آخرین yank را paste می‌کند، و "_dd که با register سیاه‌چاله حذف می‌کند بدون آنکه چیزی در حافظه بنویسد. همین دو ترفند بلافاصله سطح کارت را بالا می‌برد.


۷. Undo، redo و ارزان‌ترین superpower: نقطه

u        undo        Ctrl-r   redo
U        همه‌ی تغییرات روی آخرین خط را برگردان (خودش یک تغییر است)
.        آخرین تغییر را دوباره اجرا کن

. کوچک‌ترین دستور با بیشترین بازده است. هر تغییری در normal mode — یک ciw با متن جدید، یک dd، یک >> — با یک . تکرار می‌شود. الگویی که باید در انگشتانت بنشیند: یک بار تغییر بده، بعد فقط حرکت کن و . بزن.

/timeout      ← پیدا کردن اولین مورد
ciw duration  ← عوض کردنش
Esc
n .           ← بعدی، تکرار      n .    ← بعدی، تکرار

چرا گاهی . بهتر از :%s است؟ چون :%s/old/new/g کور است و در کل فایل اعمال می‌شود، حتی جایی که نباید. الگوی n + . کنترل مورد‌به‌مورد می‌دهد بدون از دست دادن سرعت. معیار تصمیم: بیش از ۱۵ مورد با الگوی دقیق → :%s با flag تأیید (c)؛ کمتر یا با استثنا → n + ..

undo یک درخت است، نه یک پشته. اگر چند بار undo کنی و بعد چیز جدیدی بنویسی، شاخه‌ی قبلی پاک نمی‌شود:

:earlier 10   ده تغییر عقب        :earlier 5m  وضعیت پنج دقیقه پیش
:earlier 1f   وضعیت در زمان آخرین write        :later 30s
g-  /  g+     حرکت در کل درخت undo بر اساس زمان      :undolist

:earlier 1f یعنی «فایل را به همان چیزی برگردان که آخرین بار ذخیره کردم» — بدون از دست دادن امکان برگشت.

برای اینکه تاریخچه‌ی undo بعد از بستن vim هم بماند، set undofile و set undodir=~/.vim/undo// را در vimrc بگذار (و دایرکتوری را بساز). دو اسلش انتهایی یعنی نام فایل undo از مسیر کامل ساخته شود تا فایل‌های هم‌نام تداخل نکنند. بخش امنیت را هم بخوان: این فایل‌ها محتوای قدیمی را روی دیسک نگه می‌دارند.

«تفاوت `u` و `U` و `Ctrl-r` چیست؟»

u آخرین تغییر را برمی‌گرداند و پشت سر هم قابل تکرار است؛ Ctrl-r عکس آن (redo). اما U همه‌ی تغییرات روی آخرین خط ویرایش‌شده را یک‌جا برمی‌گرداند — و خودش هم یک تغییر است، پس زدن دوباره‌اش آن را برمی‌گرداند (toggle می‌کند).

جواب سنیورتر: اضافه کن که تاریخچه‌ی undo در vim یک درخت است نه لیست خطی، با :earlier/:later/g-/g+ قابل پیمایش است، و با set undofile بین sessionها ماندگار می‌شود.


۸. Insert mode: ورود هوشمند و ترفندهای داخل آن

کلید معنی
i / a insert قبل از / بعد از مکان‌نما
I / A ابتدای متن خط (بعد از تورفتگی) / انتهای خط
o / O خط جدید زیر / بالا و insert
gi insert دقیقاً جایی که آخرین بار insert را ترک کردی
cw cc C s S حذف + insert در یک حرکت

برای گذاشتن سمی‌کالن در انتهای خط، نه $ سپس a سپس ; — فقط A; و Esc.

Ctrl-w    کلمه‌ی قبلی را پاک کن       Ctrl-u  تا ابتدای متن درج‌شده را پاک کن
Ctrl-t    یک سطح تورفتگی اضافه کن     Ctrl-d  یک سطح کم کن
Ctrl-r a  محتوای register a را درج کن
Ctrl-r =  یک عبارت را محاسبه و درج کن، مثلا  Ctrl-r = 24*60  → 1440
Ctrl-o    یک فرمان normal اجرا کن و برگرد به insert
Ctrl-n / Ctrl-p   تکمیل کلمه از bufferهای باز
Ctrl-x Ctrl-f     تکمیل مسیر فایل      Ctrl-x Ctrl-l  تکمیل کل یک خط

Ctrl-n را جدی بگیر: بدون هیچ پلاگینی، سه حرف اول applicationEventPublisher و یک Ctrl-n کافی است. در Vim 9.2 با set completeopt+=fuzzy تطبیق fuzzy هم فعال می‌شود (جزئیات در :help 'completeopt'؛ در نسخه‌های قدیمی‌تر این گزینه وجود ندارد).

paste کردن از کلیپ‌بورد ترمینال، تورفتگی را نابود می‌کند

اگر در insert mode با Ctrl-Shift-V ترمینال یک بلوک کد را paste کنی، vim آن را «تایپ‌شده» می‌بیند و autoindent روی هر خط دوباره تورفتگی اضافه می‌کند — نتیجه یک نردبان مورب است.

راه درست: در normal mode "+p بزن تا از register کلیپ‌بورد paste شود؛ تورفتگی دست‌نخورده می‌ماند. در ترمینال‌های امروزی با bracketed paste (Vim 8.0 به بعد) معمولاً مشکل خودبه‌خود حل است. راه قدیمی :set paste / :set nopaste هم هست اما autoindent و abbreviation را موقتاً می‌بندد و دیگر توصیه نمی‌شود.

با vim --version | grep clipboard ببین اصلاً register کلیپ‌بورد داری یا نه؛ نسخه‌ی حداقلی روی سرورها معمولاً -clipboard است.


۹. جستجو و substitute

/pattern   جستجو به جلو        ?pattern  جستجو به عقب
n          تطبیق بعدی           N         تطبیق قبلی
*          کلمه‌ی زیر مکان‌نما را به جلو جستجو کن (با مرز کلمه)
#          همان، به عقب        g* / g#   بدون مرز کلمه (زیررشته هم پیدا می‌شود)
:noh       خاموش کردن هایلایت نتایج

* را زیاد استفاده خواهی کرد: مکان‌نما را روی نام یک متغیر بگذار و * بزن تا همه‌ی استفاده‌هایش را ببینی.

set incsearch    " همزمان با تایپ، به اولین تطبیق بپر
set hlsearch     " همه‌ی تطبیق‌ها را هایلایت کن
set ignorecase   " به بزرگی/کوچکی حروف حساس نباش
set smartcase    " مگر اینکه خودت حرف بزرگ تایپ کنی

این دو تای آخر با هم یعنی: /user هم user و هم User را پیدا می‌کند، اما /User فقط User را — دقیقاً همان چیزی که مغز آدم انتظار دارد.

مبانی regex در vim

.        هر کاراکتر            *  صفر یا بیشتر از قبلی
^  $     ابتدا / انتهای خط     \|  یا (or)
\<  \>   مرز ابتدا/انتهای کلمه
\d \w \s رقم / کاراکتر کلمه / فاصله    (\D \W \S معکوس‌ها)
[abc]    مجموعه       [^abc]  نفی
\(...\)  گروه (بک‌اسلش لازم است)      \1 \2  ارجاع به گروه
\+  \?   یک‌یا‌بیشتر / صفر‌یا‌یک        \{2,5}  بازه‌ی تکرار
\zs \ze  ابتدا/انتهای بخشی که واقعا «تطبیق» حساب می‌شود

بک‌اسلش‌های اضافه آزاردهنده‌اند؛ پیشوند \v («very magic») نحو را به چیزی نزدیک PCRE می‌برد:

/\v(get|set)[A-Z]\w+     ← بدون \v باید بنویسی /\(get\|set\)[A-Z]\w\+
`\v` را عادت کن

اگر با regex در جاوا، grep -E یا JavaScript راحتی، همیشه الگویت را با \v شروع کن. یک کاراکتر بیشتر تایپ می‌کنی و ده بک‌اسلش کمتر. در :%s که الگو طولانی می‌شود، همین تفاوت بین «کار می‌کند» و «نیم ساعت debug کردن regex» است.

Substitute: :s

:[range]s/{pattern}/{replacement}/{flags}
range معنی
(خالی) فقط خط فعلی
% کل فایل
1,10 / .,$ / .,+5 خط ۱ تا ۱۰ / از خط فعلی تا انتها / خط فعلی و پنج خط بعد
'a,'b از mark a تا mark b
'<,'> آخرین انتخاب visual (خودکار درج می‌شود)
/start/,/end/ از خط تطبیق‌یافته با start تا خط تطبیق‌یافته با end
flag معنی
g همه‌ی تطبیق‌های هر خط (بدون آن فقط اولی)
c برای هر تطبیق تأیید بگیر
i / I بی‌اعتنا / حساس به بزرگی حروف، فارغ از ignorecase
e اگر تطبیقی نبود خطا نده (برای اسکریپت‌ها حیاتی است)
n چیزی عوض نکن، فقط تعداد تطبیق‌ها را گزارش بده
:%s/\vlocalhost:8080/api.internal:8443/g     " جایگزینی در کل فایل
:%s/timeout/duration/gc                       " با تأیید مورد به مورد
:%s/\v\s+$//e                                 " پاک کردن فاصله‌های انتهای خط
:%s/\v^(\s*)-/\1  -/                          " دو فاصله تورفتگی به آیتم‌های YAML
:'<,'>s/,/,\r/g                               " هر کاما را به خط جدید تبدیل کن
:%s/\v(\w+)\.(\w+)/\2.\1/g                    " جابه‌جا کردن دو بخش با backreference
:%s/pattern//gn                               " فقط بشمار، چیزی عوض نکن
:%s//new/g                                    " الگوی خالی = آخرین چیزی که جستجو کردی
:%&&                                          " آخرین substitute را با همان flagها تکرار کن

با flag c، vim برای هر تطبیق منتظر یک کلید می‌ماند: y بله · n نه · a همه‌ی بقیه · q بس است · l این آخری · Ctrl-e/Ctrl-y اسکرول برای دیدن context.

بدون `%` فقط یک خط عوض می‌شود

بزرگ‌ترین تله‌ی :s این است: در sed فرمان روی کل ورودی اجرا می‌شود، اما در vim بدون range فقط روی خط فعلی. یعنی :s/old/new/g که فکر می‌کنی کل فایل را عوض کرده، یک خط را عوض کرده و تو با خیال راحت :wq می‌زنی.

عادت بساز: هر بار که منظورت کل فایل است، % را تایپ کن. و قبل از هر substitute بزرگ روی فایل واقعی، اول با flag n بشمار: :%s/old//gn تعداد را می‌گوید. اگر عدد با انتظارت نخواند، الگویت اشتباه است — نه فایل.

:global — قدرتمندترین فرمان vim

:g روی هر خطی که با الگو تطبیق دارد یک فرمان Ex اجرا می‌کند؛ :v (یا :g!) روی هر خطی که تطبیق ندارد.

:g/DEBUG/d                     " همه‌ی خطوط شامل DEBUG را حذف کن
:v/ERROR/d                     " هر خطی که ERROR ندارد را حذف کن (فیلتر لاگ)
:g/^\s*$/d                     " خطوط خالی را حذف کن
:g/TODO/normal A  <-- check    " انتهای هر خط TODO یک یادداشت اضافه کن
:g/^import/sort                " خطوط import را مرتب کن
:g/pattern/t$                  " هر خط تطبیق‌یافته را به انتهای فایل کپی کن
:g/pattern/normal @q           " macro q را روی هر خط تطبیق‌یافته اجرا کن

ترکیب :g با macro یکی از بزرگ‌ترین اهرم‌های vim است.

«یک فایل لاگ ۵۰۰ مگابایتی داری و فقط خطوط ERROR سرویس checkout را می‌خواهی. چه می‌کنی؟»

جواب سنیور با یک سؤال شروع می‌شود: «اصلاً باید ادیتور باز کنم؟» برای فایل بزرگ، ابزار درست pipeline شل است:

grep -E 'ERROR' app.log | grep 'checkout' | tail -200 > /tmp/slice.log
vim /tmp/slice.log

چون باز کردن یک فایل بزرگ در vim یعنی خواندن کل آن در حافظه، ساختن ساختار undo و اجرای syntax highlighting روی هر خط. اگر واقعاً مجبور شدی: vim -u NONE -N bigfile.log و سپس :syntax off، :set noswapfile، :set noundofile. و اگر داخل vim هستی، :v/ERROR.*checkout/d همان کار grep را می‌کند.

اشاره به اینکه در سیستم واقعی این کار باید در لایه‌ی log aggregation و observability انجام شود — نه با grep روی دیسک سرور — امتیاز اضافه دارد.


۱۰. Visual mode: انتخاب کن، بعد عمل کن

گاهی نمی‌دانی کدام motion دقیقاً محدوده‌ی موردنظرت را می‌گیرد. visual mode برای همین است: اول انتخاب می‌کنی و می‌بینی، بعد operator می‌زنی.

v  کاراکتری     V  خطی     Ctrl-v  بلوکی
o  به سر دیگر انتخاب بپر (تا از آن سمت گسترش دهی)
gv آخرین انتخاب را دوباره برگردان

بعد از انتخاب، هر operatorی که بلدی کار می‌کند: d c y > < = gU gu gq J r ~.

Visual block: ویرایش ستونی

Ctrl-v همان چیزی است که در ادیتورهای دیگر «multi-cursor» نامیده می‌شود. سناریو: می‌خواهی ده خط یک فایل .env را کامنت کنی.

1. مکان‌نما را روی ستون اول خط اول بگذار
2. Ctrl-v       ← وارد block mode
3. 9j           ← بلوک عمودی بساز
4. I            ← insert در ابتدای بلوک
5. #            ← تایپ کن
6. Esc          ← جادو اینجاست: روی همه‌ی خطوط اعمال می‌شود

نکته‌ی حیاتی: متن روی بقیه‌ی خطوط تا وقتی Esc نزنی ظاهر نمی‌شود. خیلی‌ها فکر می‌کنند کار نکرده و رهایش می‌کنند.

کلید معنی
I / A درج در ابتدای / انتهای بلوک روی همه‌ی خطوط
$A افزودن در انتهای هر خط، حتی با طول‌های متفاوت
d / x / c حذف / تغییر بلوک ستونی
r{char} پر کردن کل بلوک با یک کاراکتر
y کپی بلوکی (بعداً بلوکی paste می‌شود)
دو راه برای یک کار، و کدام را انتخاب کنی

برای کامنت کردن ۶ خط، Ctrl-v سریع‌تر است. برای ۶۰۰ خط پراکنده که با یک الگو مشخص می‌شوند، :g/pattern/normal I# یا :%s/^/#/ بهتر است. قاعده: visual برای چیزی که می‌بینی، فرمان Ex برای چیزی که می‌توانی توصیف کنی. سنیورها غریزی تشخیص می‌دهند کدام حالت است.


۱۱. Registerها: کلیپ‌بورد چندگانه‌ی vim

vim یک کلیپ‌بورد ندارد؛ ده‌ها تا دارد. هر کدام یک register است و با " آدرس‌دهی می‌شود:

"ayy   خط را در register a کپی کن       "Ayy  به انتهای register a اضافه کن
"ap    محتوای register a را paste کن     :reg  محتوای همه‌ی registerها
register محتوا
"" بی‌نام — آخرین yank یا delete (پیش‌فرض p)
"0 فقط آخرین yank؛ حذف‌ها اینجا نمی‌آیند
"1"9 تاریخچه‌ی حذف‌های چندخطی؛ "1 جدیدترین
"- آخرین حذف کوچک (کمتر از یک خط)
"_ سیاه‌چاله — بنویس تا چیزی ذخیره نشود
"+ / "* کلیپ‌بورد سیستم / selection اولیه‌ی X11
"% / "# نام فایل فعلی / فایل جایگزین
": / "/ / ". آخرین فرمان Ex / آخرین الگوی جستجو / آخرین متن درج‌شده
"+y  کپی در کلیپ‌بورد سیستم      "+p  paste از کلیپ‌بورد سیستم
"_dd حذف بدون خراب کردن حافظه‌ی کپی    "0p  paste آخرین yank
q:   پنجره‌ی تاریخچه‌ی فرمان‌ها (قابل ویرایش؛ با Enter اجرا می‌شود)   q/  همان برای جستجو

روی سرور یا container حداقلی، vim معمولاً بدون پشتیبانی X11 کامپایل شده و "+y خطای E353 می‌دهد. آنجا یا از انتخاب ماوسِ خود ترمینال استفاده کن (با نگه‌داشتن Shift وقتی mouse=a روشن است)، یا فایل را با scp منتقل کن — کپی-پیست دستی ۲۰۰ خط از یک session SSH اتلاف وقت است.

«چطور متنی را از یک فایل به فایل دیگر منتقل می‌کنی، بدون کلیپ‌بورد سیستم؟»

register نام‌دار: در فایل اول "ay5j، بعد Ctrl-^ یا :e otherfile، و آنجا "ap. نکته‌ی امتیازآور: registerها بین bufferهای یک session مشترک‌اند پس همیشه جواب می‌دهد؛ اما بین دو instance مجزای vim مشترک نیستند مگر با تنظیم viminfo یا استفاده از register کلیپ‌بورد — که در آن حالت ساده‌تر است در یکی :w /tmp/chunk و در دیگری :r /tmp/chunk بزنی.


۱۲. Macroها: ضبط یک ویرایش، پخش هزار بار

macro یعنی «هرچه از الان می‌زنم را در یک register ضبط کن تا بعداً پخشش کنم». قوی‌ترین ابزار vim برای کارهای تکراری است.

نمودار: چرخه‌ی ضبط و پخش macro — Macro record-and-replay loop.

flowchart LR
    A[q then a register letter] --> B[perform the edit normally]
    B --> C[q to stop recording]
    C --> D[@a to replay once]
    D --> E[@@ to repeat the last replay]
    D --> F[10@a to replay ten times]
    D --> G[global command with normal @a]

فرض کن لیستی از نام‌ها داری و می‌خواهی هر خط را به "alpha", تبدیل کنی:

qa          ← شروع ضبط در register a
I"          ← ابتدای خط، insert، یک کوتیشن
Esc
A",         ← انتهای خط، کوتیشن و کاما
Esc
j           ← برو خط بعد ← این کلید حیاتی است
q           ← پایان ضبط
20@a        ← بیست بار دیگر اجرا کن

چهار قاعده که یک macro را قابل‌اتکا می‌کند:

۱. با یک motion مطلق شروع کن (0 یا ^) تا به موقعیت اولیه‌ی مکان‌نما وابسته نباشد. ۲. با حرکت به آیتم بعدی تمام کن — معمولاً j یا n. ۳. از count بزرگ نترس؛ وقتی macro به انتهای فایل برسد یا فرمانی شکست بخورد، vim خودش بقیه را متوقف می‌کند. پس لازم نیست دقیق بشماری. ۴. به‌جای شمردن از :g استفاده کن:g/^\s*log\./normal @a فقط روی خطوط مهم اجرا می‌شود.

macro فقط یک رشته در یک register است، پس قابل ویرایش هم هست: در یک buffer خالی "ap بزن، کلیدهای خام را اصلاح کن، و با 0"ay$ دوباره در register بگذارش — یا مستقیم :let @a='...'.

macro روی داده‌ی ناهمگون خطرناک است

macro کور است. اگر خط پنجم فرمت متفاوتی داشته باشد (کوتیشن دارد، یا خالی است)، macro همان کلیدها را می‌زند و خروجی خراب تولید می‌کند — و چون سریع اجرا می‌شود، ممکن است ۳۰۰ خط را خراب کند و بعداً بفهمی.

سه محافظ: (۱) اول روی یک آیتم اجرا کن و نتیجه را نگاه کن؛ (۲) بعد 10@a و باز هم نگاه کن؛ (۳) قبل از شروع :w بزن تا :e! همیشه راه برگشت باشد. توجه کن هر تکرار macro یک تغییر جداگانه است، پس undo کردنش چند بار u می‌خواهد — یا یک :earlier 1f.

«تفاوت `.` و macro چیست و کِی کدام را انتخاب می‌کنی؟»

. آخرین تغییر را تکرار می‌کند — یک عملیات اتمی مثل ciw یا dd. macro یک دنباله‌ی کامل از کلیدها را تکرار می‌کند، شامل حرکت‌ها، چند تغییر و حتی فرمان‌های Ex.

قاعده: اگر کار یک تغییر ساده است و می‌توانی بین موردها با n یا } حرکت کنی، . سبک‌تر و قابل‌کنترل‌تر است. اگر کار چند مرحله دارد، macro لازم است. و جواب سنیورتر: اگر کار را می‌شود با یک الگو توصیف کرد، هیچ‌کدام — یک :%s یا :g/…/normal بنویس که declarative است، در تاریخچه می‌ماند، قابل بازبینی است و حتی در یک اسکریپت با vim -es هم اجرا می‌شود.


۱۳. Markها و jumpها: حافظه‌ی مکانی

ma   نقطه‌ی فعلی را mark محلی a کن (a-z داخل همین فایل)
mA   mark سراسری A (بین فایل‌ها هم کار می‌کند)
`a   دقیقا به همان خط و ستون برگرد        'a  به ابتدای همان خط
:marks   لیست همه‌ی markها

markهای خودکاری که vim خودش نگه می‌دارد: جایی که قبل از آخرین پرش بودی (toggle بین دو نقطه) · . `` محل آخرین تغییر · `` ^ محل آخرین خروج از insert · " `` محلی که آخرین بار در این فایل بودی · `` [ و `] ابتدا و انتهای آخرین yank یا paste · '< و '> `` مرزهای آخرین انتخاب visual.

Ctrl-o   به موقعیت قبلی در jumplist برگرد (back)
Ctrl-i   جلو برو در jumplist (همان Tab)
g;  g,   حرکت در changelist (تغییر قبلی / بعدی)
:jumps   لیست پرش‌ها

Ctrl-o را مثل دکمه‌ی back مرورگر ببین: بعد از یک gg، یک /pattern یا یک پرش، تو را به جایی که بودی برمی‌گرداند. و اگر می‌خواهی برگردی و همان‌جا تایپ را ادامه دهی، gi این کار را در یک کلید انجام می‌دهد.


۱۴. Buffer، window، tab: مدل ذهنی درست

واژه‌ی «tab» در vim معنایی متفاوت از مرورگر و IDE دارد:

  • Buffer = یک فایل بارگذاری‌شده در حافظه؛ لزوماً روی صفحه دیده نمی‌شود.
  • Window (split) = یک نمای نمایشی که به یک buffer نگاه می‌کند. چند window می‌توانند به یک buffer نگاه کنند.
  • Tab page = یک چیدمان از windowها؛ یعنی یک workspace، نه یک فایل.

نمودار: رابطه‌ی buffer، window و tab page — Buffers vs windows vs tab pages.

flowchart TD
    F1[(file on disk: App.java)] --> B1[buffer 1]
    F2[(file on disk: app.yml)] --> B2[buffer 2]
    B1 --> W1[window A]
    B1 --> W2[window B]
    B2 --> W3[window C]
    W1 --> T1[tab page 1]
    W2 --> T1
    W3 --> T2[tab page 2]
مفهوم چیست برای چه استفاده‌اش کن
Buffer فایل در حافظه همه‌ی فایل‌های باز جلسه — لیست فایل‌های واقعی تو
Window نمای روی یک buffer مقایسه‌ی دو فایل یا دو بخش از یک فایل
Tab page چیدمانی از windowها جدا کردن دو task مستقل (مثلاً «کد» و «تست»)
tab در vim جایگزین buffer نیست

اشتباه رایج مهاجران از IDE این است که برای هر فایل یک tab باز می‌کنند؛ بعد از ۱۵ فایل، نوار tab غیرقابل‌استفاده می‌شود.

روش درست vim: همه‌ی فایل‌ها را به‌صورت buffer باز نگه دار و با :b partialname یا Ctrl-^ بین آن‌ها بپر. tab را فقط برای جدا کردن زمینه‌های کاری استفاده کن، نه فایل‌ها.

:e path/to/file    باز کردن فایل            :ls  یا  :buffers   لیست bufferها
:b 3               رفتن به buffer شماره ۳   :b confi  رفتن با تطبیق جزئی نام
:bn  :bp  :bd      بعدی / قبلی / بستن
Ctrl-^  (Ctrl-6)   toggle بین دو buffer آخر — پرکاربردترین میان‌بر روزانه

به‌طور پیش‌فرض vim اجازه نمی‌دهد از یک buffer تغییر‌داده‌شده خارج شوی (E37). set hidden این محدودیت را برمی‌دارد و تقریباً هر کسی که جدی از vim استفاده می‌کند آن را روشن دارد؛ فقط یادت باشد :ls تغییرنشده‌ها را با + نشان می‌دهد و :wa همه را ذخیره می‌کند.

Splitها:

:sp [file]  افقی    :vs [file]  عمودی    (یا Ctrl-w s / Ctrl-w v)
Ctrl-w h j k l   رفتن به window چپ/پایین/بالا/راست
Ctrl-w H J K L   جابه‌جا کردن خود window به لبه‌ها
Ctrl-w =  اندازه‌ها را مساوی کن      Ctrl-w _  و  Ctrl-w |  حداکثر کردن
Ctrl-w c  بستن window (buffer باز می‌ماند)      Ctrl-w o  فقط همین بماند
Ctrl-w T  این window را به یک tab جدید ببر

Tab pageها: :tabnew [file] · gt / gT بعدی و قبلی · 2gt رفتن به tab دوم · :tabclose · :tabonly.

پیدا کردن فایل بدون پلاگین

با set path+=** و set wildmenu، فرمان :find UserServ<Tab> فایل را در کل درخت پروژه پیدا می‌کند و :b User<Tab> بین bufferهای باز می‌گردد. برای جستجوی محتوا:

:vimgrep /\vTODO|FIXME/ **/*.java
:copen           " پنجره‌ی quickfix با لیست نتایج
:cnext  :cprev   " رفتن به نتیجه‌ی بعدی / قبلی
set grepprg=rg\ --vimgrep\ --smart-case   " اگر ripgrep داری، :grep سریع‌تر است

توجه: ** یعنی جستجو در همه‌ی زیرشاخه‌ها، از جمله node_modules و target/؛ آن‌ها را با wildignore (در vimrc نمونه) کنار بگذار وگرنه :find روی monorepo قفل می‌کند.

netrw: مرورگر فایل داخلی

netrw همراه vim می‌آید و نیاز به نصب ندارد: :Ex (یا :Explore) دایرکتوری فایل فعلی را باز می‌کند، و :Sex / :Vex / :Lex همان را در split افقی / عمودی / پنل کناری چپ.

کلید کار
Enter / - باز کردن فایل یا ورود به دایرکتوری / یک دایرکتوری بالا
% / d / D / R ساخت فایل / ساخت دایرکتوری / حذف / تغییر نام
v / o / t باز کردن در split عمودی / افقی / tab
i / I / Ctrl-l تعویض نمای لیست / نمایش بنر / تازه‌سازی
mf سپس mc یا mm علامت‌گذاری فایل‌ها و کپی یا انتقال
netrw و مسیرهای remote — یک CVE واقعی

netrw می‌تواند فایل‌های راه دور را هم باز کند (vim scp://host//path/file) و برای این کار در پس‌زمینه فرمان شل می‌سازد. دقیقاً همین‌جا در فوریه‌ی ۲۰۲۶ یک آسیب‌پذیری تزریق فرمان (CVE-2026-28417) گزارش شد: اعتبارسنجی hostname به‌قدر کافی سخت‌گیر نبود و با یک URL دست‌ساز می‌شد کاراکترهای متای شل را عبور داد و فرمان دلخواه را با سطح دسترسی همان کاربر اجرا کرد. در patch 9.2.0073 با اعتبارسنجی سخت‌گیرانه‌ی RFC1123 و shellescape() روی host و port اصلاح شد.

دو درس: vim را روی سرورها به‌روز نگه دار، و هیچ‌وقت یک URL ناشناخته را کورکورانه در ادیتور باز نکن — همان قاعده‌ای که برای کلیک روی لینک ناشناس داری.

«`:g/pattern/normal @q` دقیقاً چه می‌کند و چرا از یک حلقه‌ی دستی بهتر است؟»

:g در دو مرحله کار می‌کند: اول کل range را اسکن می‌کند و خطوط تطبیق‌یافته را علامت می‌زند، بعد فرمان را روی آن خطوط اجرا می‌کند. normal @q یعنی macro موجود در register q را در normal mode روی هر خط علامت‌خورده پخش کن.

چرا بهتر است: (۱) مجموعه‌ی هدف قبل از هر تغییری تثبیت شده، پس ویرایش‌ها لیست خطوط را جابه‌جا نمی‌کنند — چیزی که در تکرار دستی @q مرتب باعث پریدن از روی خطوط می‌شود؛ (۲) خطوط نامرتبط اصلاً لمس نمی‌شوند، پس ریسک macroِ کور بسیار کمتر است؛ (۳) کل عملیات یک فرمان قابل بازبینی است که در تاریخچه می‌ماند و می‌شود قبل از اجرا خواندش.


۱۵. تنظیمات و یک .vimrc خط‌به‌خط

vim بدون config هم کار می‌کند؛ اما ده خط تنظیم، تجربه را از «قابل تحمل» به «لذت‌بخش» می‌برد.

vim فایل تنظیمات را کجا پیدا می‌کند؟ پنج جا را به ترتیب می‌گردد و اولین موردی که پیدا شود برنده است و بقیه نادیده گرفته می‌شوند. اگر هیچ‌کدام نبود، $VIMRUNTIME/defaults.vim بارگذاری می‌شود که یک پیکربندی معقول مدرن دارد (nocompatible، incsearch، syntax on، filetype plugin indent on، scrolloff=5).

نمودار: ترتیب پیدا کردن فایل تنظیمات هنگام راه‌اندازی — vimrc lookup order at startup.

flowchart TD
    A[vim starts] --> B[system vimrc is read first]
    B --> C{VIMINIT env var set}
    C -- yes --> Z[first match wins, stop searching]
    C -- no --> D{HOME/.vimrc exists}
    D -- yes --> Z
    D -- no --> E{HOME/.vim/vimrc exists}
    E -- yes --> Z
    E -- no --> F{XDG_CONFIG_HOME/vim/vimrc exists}
    F -- yes --> Z
    F -- no --> G[load defaults.vim from VIMRUNTIME]
مسیر XDG در Vim مدرن

از patch 9.1.0327 به بعد (و طبیعتاً در Vim 9.2)، vim روی یونیکس $XDG_CONFIG_HOME/vim/vimrc — یعنی به‌طور پیش‌فرض ~/.config/vim/vimrc — را هم می‌خواند. اما فقط اگر ~/.vimrc و ~/.vim/vimrc وجود نداشته باشند؛ یعنی سازگاری با گذشته حفظ شده. اگر می‌خواهی به مسیر XDG مهاجرت کنی، باید دو فایل قدیمی را واقعاً جابه‌جا کنی. وقتی از این مسیر استفاده شود، runtimepath و packpath هم خودکار به $XDG_CONFIG_HOME/vim تنظیم می‌شوند. با :echo $MYVIMRC ببین کدام فایل عملاً بارگذاری شده — این سریع‌ترین راه برای دیباگ «چرا تنظیماتم اعمال نمی‌شود».

یک .vimrc شروع‌کننده که هر خطش دلیل دارد:

" ---------- پایه ----------
set nocompatible          " رفتار مدرن vim، نه شبیه‌سازی vi باستانی
filetype plugin indent on " تشخیص نوع فایل + پلاگین و تورفتگی مخصوص آن
syntax on                 " رنگ‌آمیزی نحوی
set encoding=utf-8

" ---------- نمایش ----------
set number relativenumber " شماره‌ی خط مطلق روی خط فعلی، نسبی روی بقیه
set laststatus=2          " نوار وضعیت همیشه روشن
set scrolloff=5           " همیشه ۵ خط بالا و پایین مکان‌نما دیده شود
set showcmd wildmenu      " نمایش فرمان نیمه‌تایپ‌شده + منوی تکمیل

" ---------- جستجو ----------
set incsearch hlsearch
set ignorecase smartcase

" ---------- تورفتگی ----------
set expandtab             " Tab را به space تبدیل کن
set tabstop=4             " یک کاراکتر Tab چند ستون دیده شود
set softtabstop=4         " فشردن Tab چند فاصله درج کند
set shiftwidth=4          " اندازه‌ی یک سطح تورفتگی برای >> و <<
set autoindent smartindent

" ---------- رفتار فایل ----------
set hidden                " رفتن از buffer تغییر‌داده‌شده بدون ذخیره مجاز باشد
set autoread              " اگر فایل بیرون عوض شد، دوباره بخوان
set undofile
set undodir=~/.vim/undo//   " تاریخچه‌ی undo بین sessionها بماند
set directory=~/.vim/swap//

" ---------- ناوبری ----------
set path+=**              " :find بازگشتی در درخت پروژه
set wildignore+=**/node_modules/**,**/target/**,**/.git/**,**/build/**
set splitbelow splitright " splitهای جدید پایین و راست باز شوند
set mouse=a               " ماوس فعال (تغییر اندازه‌ی split و اسکرول)

" ---------- امنیت ----------
set nomodeline            " اجرای تنظیمات جاسازی‌شده در فایل‌ها را خاموش کن

قبل از اجرا، mkdir -p ~/.vim/{undo,swap} را فراموش نکن، وگرنه vim موقع نوشتن خطا می‌دهد.

سه فرمان برای دیباگ کردن config:

:so %                     " همین فایل vimrc را دوباره بارگذاری کن (بدون restart)
:set shiftwidth?          " مقدار فعلی یک گزینه را ببین
:verbose set shiftwidth?  " و ببین کدام فایل و کدام خط آن را ست کرده

:verbose set X? طلا است: وقتی یک پلاگین یا یک ftplugin تنظیمات تو را بازنویسی می‌کند، این فرمان دقیقاً متهم را نشان می‌دهد.


۱۶. پلاگین‌ها: به‌اختصار و با قضاوت

از Vim 8 به بعد، package manager داخلی داری و برای پلاگین‌های ساده به هیچ ابزار بیرونی نیاز نیست:

mkdir -p ~/.vim/pack/mine/start          # پلاگین‌هایی که همیشه بارگذاری شوند
mkdir -p ~/.vim/pack/mine/opt            # پلاگین‌هایی که با :packadd دستی بیایند
git clone https://github.com/tpope/vim-fugitive ~/.vim/pack/mine/start/fugitive
vim -c "helptags ALL" -c q          # ایندکس کردن فایل‌های help پلاگین جدید

و اگر پلاگین‌های زیاد داری، vim-plug رایج‌ترین انتخاب سبک است:

curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
  https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
call plug#begin('~/.vim/plugged')
Plug 'tpope/vim-surround'      " تغییر پرانتز/کوتیشن دور یک متن
Plug 'tpope/vim-commentary'    " gcc برای کامنت کردن
Plug 'tpope/vim-fugitive'      " git داخل vim
call plug#end()

سپس داخل vim: :PlugInstall نصب، :PlugUpdate به‌روزرسانی، :PlugClean حذف پلاگین‌هایی که از config برداشته‌ای، :PlugUpgrade به‌روزرسانی خود vim-plug.

رویکرد مزیت هزینه مناسب برای
بدون پلاگین همه‌جا یکسان، صفر نگهداری، صفر تأخیر باید :find، :vimgrep و Ctrl-n را بلد باشی سرورها، containerها، ماشین‌های موقت
package داخلی (pack/*/start) بدون وابستگی بیرونی، با git قابل نسخه‌بندی به‌روزرسانی دستی ست کوچک و پایدار از پلاگین‌ها
vim-plug نصب موازی، lazy-load، دستورهای ساده یک وابستگی بیرونی ماشین کاری اصلی با ده‌ها پلاگین
config شخصی تو روی سرور وجود ندارد

تله‌ی بزرگ کسانی که vim را «سنگین» می‌کنند: آن‌قدر به <leader> mapها و پلاگین‌ها عادت می‌کنند که روی یک سرور خام یا داخل یک container کاملاً فلج می‌شوند — چون آنجا نه .vimrc هست، نه پلاگین، نه حتی پشتیبانی کلیپ‌بورد.

قاعده‌ی سنیور: مهارت پایه‌ات باید روی vim -u NONE هم کار کند. پلاگین‌ها را برای راحتی اضافه کن، نه برای جبران ندانستن. و هر از گاهی startup را بسنج: vim --startuptime /tmp/start.log +q و بعد sort -k2 -nr /tmp/start.log | head — اگر بالای ۲۰۰ میلی‌ثانیه است، یک پلاگین دارد وقتت را می‌خورد.


۱۷. vim داخل git

git برای پیام commit، interactive rebase، git add -p با گزینه‌ی e و git tag -a ادیتور باز می‌کند. کدام ادیتور؟ ترتیب اولویت مشخص است:

نمودار: انتخاب ادیتور توسط git — How git resolves which editor to launch.

flowchart TD
    A[git needs an editor] --> B{GIT_EDITOR env var}
    B -- set --> Z[launch it]
    B -- not set --> C{core.editor in git config}
    C -- set --> Z
    C -- not set --> D{VISUAL env var}
    D -- set --> Z
    D -- not set --> E{EDITOR env var}
    E -- set --> Z
    E -- not set --> F[fall back to vi]
git config --global core.editor "vim"
git config --global core.editor "vim -f"   # -f یعنی fork نکن؛ برای GUIها لازم است
export EDITOR=vim                          # ابزارهای دیگر هم از این استفاده می‌کنند
export KUBE_EDITOR=vim                     # kubectl اول KUBE_EDITOR بعد EDITOR را می‌بیند

kubectl edit هم دقیقاً همین الگو را دارد: اول KUBE_EDITOR، بعد EDITOR، و در نهایت vi روی لینوکس و notepad روی ویندوز. crontab -e و visudo هم از VISUAL/EDITOR تبعیت می‌کنند.

نوشتن یک پیام commit خوب در vim

خط اول را خلاصه و امری بنویس، یک خط خالی بگذار، بدنه را تایپ کن و با gqap آن را به عرض textwidth بشکن (قرارداد رایج: ۷۲ ستون)؛ بعد ZZ.

مهم‌ترین ترفند: :cq یعنی «با کد خطا خارج شو». git این را «کاربر منصرف شد» تفسیر می‌کند و commit یا rebase را لغو می‌کند — قطعی‌تر از پاک کردن متن، و در وسط یک git rebase -i تفاوت بین «لغو» و «اجرای یک plan نیمه‌کاره» است.

diff و merge

git config --global diff.tool vimdiff
git config --global merge.tool vimdiff
git difftool HEAD~1 -- src/App.java     # مقایسه در vim
git mergetool                            # حل conflict در چیدمان چهارپنجره‌ای

git mergetool --tool=vimdiff چهار پنجره باز می‌کند: LOCAL · BASE · REMOTE در بالا و فایل ادغام‌شده (MERGED) در پایین. چیدمان با mergetool.vimdiff.layout قابل تغییر است (+ یعنی tab جدید، , یعنی split عمودی).

کلیدهای حالت diff:

]c   /  [c      پرش به تغییر بعدی / قبلی
do  (diff obtain)   تغییر را از پنجره‌ی دیگر بگیر
dp  (diff put)      تغییر را به پنجره‌ی دیگر بفرست
:diffget LOCAL      در merge چهارپنجره‌ای: از buffer نام‌دار بگیر
:diffput            به buffer دیگر بفرست
:diffupdate         بعد از ویرایش دستی، diff را بازمحاسبه کن

در Vim 9.2 حالت diff بهبود یافته و الگوریتم linematch تغییرات را بر اساس شباهت خطوط هم‌تراز می‌کند؛ برای conflictهای شلوغ خیلی خواناتر است:

set diffopt+=linematch:60
«در وسط `git rebase -i` هستی و ادیتور باز شده. چطور یک commit را squash می‌کنی و چطور کل عملیات را امن لغو می‌کنی؟»

در فایل todo که باز می‌شود، کلمه‌ی pick جلوی commit دوم را به squash (یا s) عوض می‌کنم — با مکان‌نما روی آن کلمه، cw و تایپ squash و Esc. اگر چند تا باشد، الگوی n سپس . یا یک :%s محدود به range کار را تمام می‌کند. بعد ZZ.

برای لغو امن: :cq. این با exit code غیرصفر خارج می‌شود و git کل rebase را متوقف می‌کند بدون اینکه هیچ commitی بازنویسی شود. اگر rebase قبلاً شروع شده باشد، git rebase --abort وضعیت را کاملاً برمی‌گرداند. جواب ضعیف این است که آدم بگوید «همه‌ی خط‌ها را پاک می‌کنم» — کار می‌کند اما هدف را مبهم می‌گذارد.

«چرا `core.editor` را روی `vim -f` تنظیم می‌کنند؟»

-f یعنی «foreground؛ fork نکن». برای vim ترمینالی معمولاً بی‌اثر است، اما اگر ادیتور یک برنامه‌ی گرافیکی باشد (gvim, code), بدون این گزینه فرایند فوراً برمی‌گردد، git فکر می‌کند کاربر ادیتور را بست، و فایل خالی را به‌عنوان پیام commit برمی‌دارد — نتیجه: «Aborting commit due to empty commit message» یا بدتر، یک commit با پیام ناقص. برای code معادلش --wait است. این سؤال در واقع درباره‌ی فهم مدل فرایند است، نه vim.


۱۸. امنیت و بهداشت production

ادیتور روی سرور یک ابزار بی‌خطر نیست؛ چند نکته که واقعاً به کار می‌آید:

modeline. vim می‌تواند تنظیمات را از یک کامنت داخل خود فایل بخواند (# vim: set ts=2 sw=2:). این قابلیت در گذشته منجر به اجرای فرمان دلخواه شد: CVE-2019-12735 (اصلاح در Vim 8.1.1365) اجازه می‌داد باز کردن یک فایل دست‌ساز، فرمان سیستمی اجرا کند. امروز modeline محدودتر است و اجرای عبارت فقط با 'modelineexpr' — که پیش‌فرض خاموش است — ممکن می‌شود؛ اما روی ماشینی که فایل‌های ناشناخته باز می‌کنی، set nomodeline یک لایه‌ی دفاعی رایگان است.

فایل‌های ناشناخته را در محیط خنثی باز کن.

vim -u NONE -N suspicious.log     # بدون vimrc و بدون پلاگین
vim -Z suspicious.log             # restricted mode: هیچ فرمان شلی اجرا نمی‌شود
view suspicious.log               # معادل vim -R، فقط خواندن

ویرایش فایل‌های root. الگوی درست sudoedit (یا sudo -e) است: فایل را در یک کپی موقت با کاربر عادی و ادیتور خودت باز می‌کند و بعد از ذخیره، آن را با دسترسی root برمی‌گرداند. با sudo vim تو یک vim کامل با دسترسی root اجرا می‌کنی که می‌تواند :!sh بزند و هر config محلی را هم با آن دسترسی بخواند.

فایل‌های جانبی نشت اطلاعات‌اند. فایل swap (.file.swp)، فایل backup (file~) و فایل undo محتوای فایل را روی دیسک نگه می‌دارند. اگر روی سروری فایل حاوی رمز یا کلید ویرایش می‌کنی، این‌ها را برای آن مسیر خاموش کن:

autocmd BufNewFile,BufReadPre */secrets/* setlocal noswapfile nobackup noundofile viminfo=
سه اشتباهی که واقعاً روی production اتفاق می‌افتد

۱. ویرایش مستقیم config روی سرور بدون منبع کنترل. فایل را عوض می‌کنی، سرویس درست کار می‌کند، هفته‌ی بعد یک deploy جدید فایل را بازنویسی می‌کند و باگ برمی‌گردد. اگر مجبور شدی: قبلش cp nginx.conf nginx.conf.$(date +%F) و بعدش همان تغییر را در مخزن هم اعمال کن.

۲. باز گذاشتن vim روی یک فایل و رفتن. فایل swap می‌ماند، همکار بعدی هشدار E325 می‌گیرد، D می‌زند و تغییرات نجات‌نیافته نابود می‌شود.

۳. :%s روی فایلی که نخوانده‌ای. الگوی تو ممکن است داخل یک رشته یا یک secret هم تطبیق بخورد. همیشه اول با :%s/pattern//gn بشمار، و روی فایل حساس اول یک کپی بگیر.

مثال واقعی: ویرایش یک فایل migration روی سرور

فرض کن باید یک ایندکس را به یک migration اضافه کنی. متن فایل بسته به موتور دیتابیس فرق دارد:

-- V12__add_index_on_orders.sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_customer_created
    ON orders (customer_id, created_at DESC);
ANALYZE orders;

کار vim اینجا چیست؟ اگر بخواهی همین ایندکس را برای چند جدول تکرار کنی: بلوک را با Vjj انتخاب کن، y بزن، چند بار p کن و بعد نام جدول را با :%s/\vorders/order_items/g عوض کن. vim فقط متن را عوض می‌کند؛ درستی خود migration موضوع فصل‌های sql-mastery و rdbms-tuning است.

هرگز یک migration اجراشده را ویرایش نکن

اگر این فایل قبلاً روی هیچ محیطی اجرا شده، ویرایش آن checksum ابزار migration را می‌شکند و deploy بعدی fail می‌شود. فایل جدید بساز. این را در مصاحبه هم می‌پرسند و ربطی به ادیتور ندارد — اما اینکه بدانی «ابزار سریع بودن، مجوز دور زدن فرایند نیست» دقیقاً همان قضاوتی است که از یک سنیور انتظار می‌رود.

«چطور یک فایل که با کاربر عادی باز کرده‌ای اما متعلق به root است را ذخیره می‌کنی؟»

جواب استاندارد: :w !sudo tee % > /dev/null — یعنی محتوای buffer را به ورودی sudo tee بده تا با دسترسی root روی همان فایل (%) بنویسد و خروجی را دور بریز. بعد :e! بزن تا vim فایل را دوباره بخواند.

اما جواب سنیور ادامه می‌دهد: این راه‌حل عوارض دارد — مالکیت و ACL فایل ممکن است عوض شود، در برخی تنظیمات sudo رمز را در وسط عملیات می‌خواهد و ترمینال گیج می‌شود، و اگر فایل symlink باشد رفتار غیرمنتظره می‌دهد. راه درست از اول sudoedit /etc/nginx/nginx.conf است، که فایل را به‌صورت امن روی یک کپی موقت ویرایش می‌کند. اشاره به sudoedit تفاوت بین «ترفند بلدم» و «مدل امنیتی را می‌فهمم» است.

«چطور vim را بدون هیچ config و پلاگینی اجرا می‌کنی و چرا؟»

vim -u NONE -N file. گزینه‌ی -u NONE هیچ vimrcی نمی‌خواند و پلاگین‌ها را بارگذاری نمی‌کند؛ -N هم nocompatible را روشن می‌کند تا رفتار مدرن داشته باشی (بدون آن، -u NONE تو را به حالت سازگار با vi می‌برد که حتی undo چندمرحله‌ای ندارد).

سه کاربرد واقعی: (۱) دیباگ — وقتی نمی‌دانی مشکل از خود vim است یا از پلاگین‌ها، این اولین قدم bisect است؛ (۲) کارایی روی فایل‌های بسیار بزرگ؛ (۳) امنیت، برای باز کردن فایل‌های ناشناخته، ترجیحاً همراه با -Z که restricted mode است و اجرای فرمان شل را کاملاً می‌بندد.


۱۹. کِی به‌جای vim از nano استفاده کن

nano (نسخه‌ی پایدار امروزی: ۹.۱) یک ادیتور غیرمُدال با منوی کلیدها در پایین صفحه است: Ctrl-O ذخیره، Ctrl-X خروج، Ctrl-W جستجو، Ctrl-K بریدن خط، Ctrl-U چسباندن. اگر روی سرور نصب است و کار تو یک تغییر ۳۰ ثانیه‌ای است، استفاده از آن نشانه‌ی ضعف نیست — نشانه‌ی انتخاب ابزار متناسب است.

معیار vim nano
در دسترس بودن تقریباً همه‌جا (حداقل vi) معمولاً هست، اما نه همیشه در containerهای حداقلی
منحنی یادگیری تند؛ چند روز تا راحتی تقریباً صفر
ویرایش ۱ خط در یک config کافی است کاملاً کافی و سریع‌تر
refactor تکراری روی ۳۰۰ خط macro و :g — چند ثانیه عملاً غیرممکن
فایل بزرگ (چند صد مگابایت) قابل تنظیم برای بقا معمولاً خیلی کند
diff/merge داخل git یکپارچه (vimdiff) ندارد
ریسک خرابکاری ناخواسته بالاتر (یک dd اشتباه) پایین‌تر
قاعده‌ی تصمیم

اگر می‌دانی دقیقاً چه یک تغییری می‌خواهی بدهی و کمتر از یک دقیقه است، هر ادیتوری که دم دستت است درست است. اگر کار تکرارشونده، الگودار یا اکتشافی است، vim اهرم واقعی می‌دهد. و برای فایل‌های بزرگ یا کارهایی که می‌شود توصیفشان کرد، بهترین ادیتور اصلاً ادیتور نیست: grep، sed، awk، jq و yq را هم در جعبه‌ابزارت داشته باش.


۲۰. تمرین‌های عملی

بهترین سرمایه‌گذاری اول: در ترمینال vimtutor بزن. یک آموزش تعاملی حدود ۳۰ دقیقه‌ای است که همراه خود vim نصب می‌شود و روی یک کپی موقت کار می‌کند، پس خطری ندارد.

بعد از آن، این تمرین‌ها را روی یک فایل آزمایشی انجام بده. هر کدام را تا وقتی تکرار کن که فکر نکنی:

۱. بقا: vim را باز کن، چند حرف بنویس، بدون ذخیره خارج شو. حالا ذخیره کن و خارج شو، یک بار با :wq و یک بار با ZZ. ۲. حرکت: یک فایل کد باز کن. فقط با gg, G, }, {, Ctrl-d, Ctrl-u, f, ; حرکت کن. کلیدهای جهت‌نما را ممنوع کن. ۳. گرامر: روی یک تابع، این‌ها را بزن: ciw, ci", ci(, dap, yi{, >ap, gUiw. هر بار u بزن و دوباره. ۴. نقطه: یک کلمه را در پنج جای فایل با * پیدا کن و با ciw + n + . عوض کن. ۵. substitute: یک :%s با flag c بنویس، بعد همان را با flag n فقط بشمار. ۶. بلوک: ده خط را با Ctrl-v + I + # کامنت کن، بعد با Ctrl-v + x از کامنت خارجشان کن. ۷. macro: یک لیست ۲۰ خطی از نام‌ها را به آرایه‌ی JSON تبدیل کن؛ اول با @a یکی، بعد 19@a. ۸. register و پیمایش: پنج خط را در register b بگذار و در فایل دیگری paste کن؛ بعد با ma، `a و Ctrl-o بین نقاط رفت‌و‌برگشت کن. ۹. **git:** یک commit بدون -m بساز، پیام را با gqap فرمت کن، و یک بار هم با :cq لغوش کن. ۱۰. **کمک گرفتن:** :help motion.txt را باز کن، با Ctrl-] روی یک تگ برو و با Ctrl-t برگرد. ناوبری در :help مهم‌تر از حفظ کردن هر دستور دیگری است.


۲۱. برگه‌ی تقلب: دستورهایی که ۹۵٪ کار را پوشش می‌دهند

گروه دستورها
خروج :w ذخیره · :q خروج · :wq / ZZ ذخیره+خروج · :q! / ZQ دور ریختن · :x ذخیره فقط اگر تغییر بوده · :qa! همه
حالت‌ها Esc / Ctrl-[ نرمال · i a insert · I A ابتدا/انتهای خط · o O خط جدید · v V Ctrl-v visual · R جایگزینی
حرکت خطی h j k l · 0 ستون صفر · ^ اول متن · $ آخر خط · w b e کلمه · W B E کلمه‌ی بزرگ
حرکت فایل gg اول · G آخر · 42G خط ۴۲ · { } پاراگراف · % جفت پرانتز · Ctrl-d Ctrl-u نیم‌صفحه · zz وسط‌چین
هدف‌گیری f{c} تا کاراکتر · t{c} تا قبل از آن · F T معکوس · ; , تکرار
حذف و تغییر x کاراکتر · dd خط · D تا آخر خط · dw کلمه · cw تغییر کلمه · cc تغییر خط · C تا آخر خط · s r کاراکتر
کپی/paste yy خط · yw کلمه · p بعد · P قبل · "0p آخرین yank · "_dd حذف بدون ثبت · "+y کلیپ‌بورد
text object iw aw کلمه · i" a" کوتیشن · i( a( پرانتز · i{ a{ آکولاد · ip ap پاراگراف · it at تگ
تکرار/undo . تکرار آخرین تغییر · u undo · Ctrl-r redo · U کل خط · :earlier 1f تا آخرین ذخیره
جستجو /x ?x · n N · * # کلمه‌ی زیر مکان‌نما · :noh خاموش کردن هایلایت
جایگزینی :%s/a/b/g کل فایل · :%s/a/b/gc با تأیید · :%s/a//gn فقط شمارش · :g/x/d حذف خطوط · :v/x/d نگه‌داشتن خطوط
visual > < تورفتگی · = فرمت · gU gu بزرگ/کوچک · J چسباندن خطوط · gv انتخاب قبلی
register/macro "ayy کپی در a · :reg لیست · qaq ضبط · @a پخش · @@ تکرار · 10@a ده بار
mark/jump ma علامت · `a پرش · Ctrl-o Ctrl-i تاریخچه · g; تغییر قبلی · % جفت
فایل و پنجره :e f باز · :ls لیست · :b n رفتن · Ctrl-^ toggle · :sp :vs split · Ctrl-w hjkl جابه‌جایی · gt tab بعدی
پروژه :find x با path+=** · :vimgrep /x/ **/*.java · :copen :cnext quickfix · :Ex مرورگر فایل
diff ]c [c تغییر بعدی/قبلی · do گرفتن · dp فرستادن · :diffupdate
شل :!cmd اجرا · :r !cmd درج خروجی · :%!sort فیلتر کل فایل · Ctrl-z سپس fg
کمک :h {موضوع} · Ctrl-] دنبال کردن تگ · Ctrl-t برگشت · :h index فهرست کامل کلیدها
جمع‌بندی

vim یک مجموعه از میان‌برهای عجیب نیست؛ یک زبان کوچک برای ویرایش متن است. سه لایه دارد و هر لایه ارزش خودش را دارد. لایه‌ی بقاEsc، :q!، :wq — تو را از هر سروری بیرون می‌آورد و یک ساعت وقت می‌خواهد. لایه‌ی روان بودن — motionها، گرامر operator + motion، text objectها، . و u — کاری می‌کند که ویرایش روی ترمینال کندتر از IDE نباشد و چند هفته تمرین می‌خواهد. لایه‌ی اهرم:s با range و flag، :g، registerها، macroها، quickfix — همان جایی است که vim کارهایی می‌کند که در ادیتورهای دیگر ساده‌تر نیستند، فقط ممکن نیستند.

قضاوت سنیوری که واقعاً سنجیده می‌شود، در خود دستورها نیست: بدانی چه وقت اصلاً نباید ادیتور باز کنی (فایل ۵۰۰ مگابایتی → grep)، چه وقت nano کافی است، چرا sudoedit از sudo vim بهتر است، چرا مهارتت باید روی vim -u NONE هم کار کند، و چرا تغییر دستی یک config روی production — هرچقدر هم سریع — بدهی فنی می‌سازد. ابزار را یاد بگیر، اما فرایند را دور نزن.

One day you SSH into a production server to change one line in a config file and discover the machine has no IDE and no GUI — just a terminal and an editor that has been on every Unix system for decades: vim.

Or, more simply: you run git commit without -m, the screen changes, everything you type does something strange, and Ctrl-C does not save you. That is the famous "stuck in vim" moment.

This chapter starts from zero — assuming you do not even know how to quit — and goes far enough that you can do a repetitive refactor with one macro in ten seconds, make the right call inside a git rebase -i, and give a senior-level answer in an interview.

Roadmap for this chapter

Survival (1–4): why vim still matters, the emergency exit card, the modal idea, opening / saving / quitting. The editing language (5–8): motions, the operator + motion grammar, undo and the dot command, entering insert mode intelligently. Real leverage (9–14): search, :s and :g, visual block, registers, macros, marks, and managing buffers / windows / tabs / netrw. Senior level (15–21): a .vimrc explained line by line plus the XDG story in Vim 9.2, plugins with judgment, vim inside git, security and production hygiene, choosing between vim and nano, drills and a cheat sheet.


1. Why vim still matters

Nobody is asking you to write an entire Spring Boot service in vim; that is what your IDE is for. But there are four situations where vim is not a preference, it is reality:

  • Servers and containers. Practically every Linux distribution ships vim or vi. Inside a pod or a VM there is no GUI, and application.yml or nginx.conf has to be changed right there.
  • git. git commit, git rebase -i, git tag -a and git add -p all open an editor, and that editor is very often vim.
  • Cloud-native tooling. kubectl edit deployment/api opens a temporary YAML in your editor and ships it to the API server when you save; crontab -e and visudo behave the same way. If you don't know the editor, you don't know the command.
  • Raw speed on text. Once you see text as structure (word, block, paragraph), an edit that takes thirty seconds elsewhere takes two seconds here.
vim is the wrench in your pocket

An IDE is a full workshop: press, crane, CNC machine. Excellent — until you get a flat tyre by the roadside, where what helps is the wrench that was always in your pocket. vim is that wrench: small, everywhere, and if you know how to hold it, it gets you out of any jam.

The version this chapter is written against

Vim 9.2 (released 14 February 2026). Check yours with vim --version. Nearly everything here works from Vim 7 onward; wherever a feature needs a specific version, it is called out explicitly.


2. Emergency rescue: how to get out of vim

Before any teaching, the exit card:

1) Press Esc  (a few times does no harm)
2) Type:  :q!   then Enter   → quit without saving
   or type:  :wq  then Enter  → save and quit

Why Esc first? Because you were probably in insert mode, so your keystrokes were being added to the text rather than issuing commands.

  • E37: No write since last change — unsaved changes: :q! to discard, :wq to save.
  • Several files open:qa! closes them all.
  • The screen is frozen — probably the terminal's Ctrl-S, not vim; Ctrl-Q releases it.
  • vim disappearedCtrl-Z suspended it, it is not dead; type fg in the shell.
The ATTENTION message and the swap file

If opening a file greets you with E325: ATTENTION ... swap file ... already exists!, then either another vim has the same file open right now, or a previous session crashed and left .filename.swp behind.

The safe option is R (Recover): inspect the recovered version, compare, then delete the swap file. Never blindly press D — you may destroy the only copy of unsaved work. On a shared server also confirm nobody else has the file open: two people editing one nginx.conf means the last writer silently erases the first one's changes.

"You are stuck in vim. Walk me through exactly what you do."

A warm-up question whose answer shows how comfortable you are in a terminal: "First Esc for normal mode. To discard, :q!; to save, :wq or ZZ; with several buffers, :qa!. If the editor is not responding at all, the terminal is probably frozen by Ctrl-S, which Ctrl-Q releases, or vim was suspended with Ctrl-Z, which fg brings back." That last sentence separates "memorised it" from "understands it".


3. The modal idea: the single most important concept here

In a normal editor the keyboard always does one thing: produce characters. Everything else needs modifiers, a mouse, or a menu — so 26 letters are "wasted" on typing alone.

vim makes a different trade: modes. In normal mode d means delete, y means yank (copy), p means paste; to type text you must explicitly enter insert mode.

A car gearbox

The same accelerator pedal means "slow and powerful" in first gear and "fast" in fifth. The pedal did not change; the mode did.

The d key does not change either; the mode does. Beginners are annoyed because they think they should always be in typing gear. Professionals know that the default is command gear — because a developer spends far more time reading and moving code than typing new code.

Mode How to enter What it does How to leave
Normal default; Esc move, delete, copy, paste, run commands
Insert i a o I A O c type text Esc or Ctrl-[
Visual v charwise · V linewise · Ctrl-v blockwise select, then operate Esc
Command-line : Ex commands · / ? search :w, :s, :e Enter or Esc

Diagram: the vim mode state machine — نمودار ماشین حالت modeها در vim.

stateDiagram-v2
    [*] --> Normal
    Normal --> Insert: i a o I A O c
    Insert --> Normal: Esc
    Normal --> Visual: v V Ctrl-v
    Visual --> Normal: Esc
    Visual --> Insert: c s
    Normal --> CommandLine: colon slash question
    CommandLine --> Normal: Enter or Esc
    Normal --> Replace: R
    Replace --> Normal: Esc
The golden rule for beginners

Do not live in insert mode. The classic mistake is to press i, stay there all session, and navigate with arrow keys — using vim as a weak Notepad.

The right habit: one thought = one trip in and out of insert. Your resting state is normal mode; a good sign of progress is your finger hitting Esc automatically after each sentence.

Esc is far away — three alternatives. Ctrl-[ is exactly equivalent and works everywhere. Ctrl-c is nearly equivalent but skips InsertLeave autocommands and abbreviations. A mapping (inoremap jk <Esc>) works too, but will not exist on someone else's machine — so learn Ctrl-[.


4. Opening, saving, quitting

vim app.yml            # open a file (created on write if missing)
vim +42 app.yml        # open and jump to line 42
vim +/timeout app.yml  # jump to the first match of "timeout"
vim -R app.yml         # read-only
vim -d old.yml new.yml # diff mode
vim -u NONE -N app.yml # no vimrc, no plugins, modern behaviour
vim -O a.py b.py       # side by side (-o for horizontal splits)
Command Meaning
:w write, stay open
:w newname write under another name (the buffer stays on the old file)
:sav newname real save-as — the buffer is rebound to the new file
:q / :q! quit / quit and discard changes
:wq write and quit (always writes)
:x write and quit — only if something actually changed
ZZ / ZQ :x / :q! without the colon
:wa :qa :wqa the same, across all buffers
:e! reload from disk, discarding local changes
Why seniors prefer `:x` over `:wq`

:wq always writes, even if you changed nothing — bumping the file's mtime. On your laptop that is harmless; on a server it can trigger an mtime-based build, wake a file watcher, or appear in an audit log and make someone think you tampered with the config. :x and ZZ write only when there is something to write.


5. Motions: moving without a mouse

A motion is a command that moves the cursor. They matter for navigation and — more importantly — because they are the input to operators.

h left    j down    k up    l right

Word-wise movement:

w   start of next word        b   start of previous word
e   end of word               ge  end of previous word
W B E   same, but "word" = anything between whitespace

On user.profile.email, w treats the dots as separators and stops three times, while W sees one word — a difference that matters on file paths, URLs and Java package names.

Within a line:

0   column zero      ^   first non-blank character
$   end of line      g_  last non-blank character      40| column 40

Within the file:

gg  first line        G     last line        42G or :42  line 42
H M L   top / middle / bottom of the screen
Ctrl-d half page down   Ctrl-u half page up
Ctrl-f page forward     Ctrl-b page back
zz  centre the current line   (zt top, zb bottom)
{   previous paragraph   }   next paragraph   ( ) sentences
%   jump to the matching bracket, brace or paren

Targeted movement with f t ; ,:

fx  forward to the next x        Fx  backward to the previous x
tx  forward to just before x     Tx  backward to just after x
;   repeat that search           ,   repeat it in the other direction

On repository.findByStatusAndCreatedAfter(status, cutoff); with the cursor at column zero, f( puts you straight on the opening paren — far better than twenty presses of l.

Counts. Every motion can be multiplied: 3w, 5j, 2}. For vertical jumps turn on set relativenumber so each line shows its distance from the cursor and 7k stops being a guess. But if you catch yourself counting lines with your eyes, you picked the wrong tool: over longer distances a search, :42, or a mark beats a count.


6. The grammar of vim: where it all clicks

Here vim stops being "an editor with strange keys" and becomes a small language for editing text:

[count] operator [count] {motion | text-object}

That is verb + object. The operator is the verb (delete, change, copy); the motion or text object is the object.

Diagram: anatomy of a vim edit command — نمودار ساختار یک فرمان ویرایشی در vim.

flowchart LR
    A[count: optional number] --> B[operator: d c y gU gq]
    B --> C[motion or text object: w e dollar ap iw]
    C --> D[operation applied to that exact range]
    B -.doubled operator.-> E[dd yy cc gUU]
    E --> D
Operator Meaning Example
d delete (also goes to a register, so it is really cut) dw d$ dap
c change (delete, then enter insert) cw ci" cc
y yank (copy) yy yi( y$
> / < indent right / left >ap >j
= re-indent automatically =ap gg=G
gu / gU / g~ lowercase / uppercase / toggle case gUiw
gq reflow text to textwidth gqap
! filter text through a shell command !apsort

A text object is a stronger idea than a motion: instead of "from here to there" you say "the thing I am inside of".

iw / aw   word (aw also grabs the trailing space)
is / as   sentence        ip / ap   paragraph
i( a(  or  ib ab   parentheses      i{ a{  or  iB aB   braces
i[ a[   brackets     i" a"  and  i' a'   quotes
it / at   inside an HTML/XML tag  /  including the tag

Read these as you would think them:

ciw   change the word you are on
ci"   change what is inside the quotes
da(   delete the parenthesised block including the parens
yi{   yank what is inside the braces
dap   delete the whole paragraph
>ip   indent this paragraph one level
gUiw  uppercase this word
Don't memorise the combinations — compose them

You are not memorising 200 shortcuts. You learn about 8 verbs and 20 objects, and their product is 160 commands you never memorised individually. Any new operator you learn later immediately combines with every object you already know — exactly the principle behind good API design: a small set of orthogonal operations instead of a pile of single-purpose commands.

Shorthand Equivalent Meaning
dd / yy delete / yank the whole line
cc (or S) change the line's content, keep the indent
D / C d$ / c$ delete / change to end of line
x / X dl / dh delete character forward / backward
s cl substitute a character and enter insert
p / P paste after / before the cursor
J / gJ join the next line with / without a space
r{char} replace one character without entering insert
Ctrl-a / Ctrl-x increment / decrement the number under the cursor

Ctrl-a is a hidden gem: put the cursor on a number and press 5Ctrl-a to add five — perfect for bumping a version in pom.xml or a port in a compose file. Note that old vim treats numbers starting with 0 as octal; modern defaults.vim disables that with set nrformats-=octal, which you must add yourself if you wrote your own vimrc.

If what you copied is "linewise" (from yy or dd), p puts it on the next line and P on the previous one; vim remembers the register's type.

The classic trap: deleting clobbers your copy

You yank a line with yy, delete another with dd to make room, press p — and get the deleted line back instead of the yanked one.

The reason: d, like y, writes into the unnamed register (""), and the last write wins. Two fixes: "0p always pastes the last yank, and "_dd deletes through the black-hole register without storing anything.


7. Undo, redo, and the cheapest superpower: the dot

u        undo        Ctrl-r   redo
U        undo all changes on the last edited line (itself a change)
.        repeat the last change

. is the smallest command with the biggest payoff: any normal-mode change — a ciw with new text, a dd, a >> — repeats with one keystroke. Burn this into your fingers: change once, then just move and press .

/timeout      ← find the first occurrence
ciw duration  ← change it
Esc
n .           ← next, repeat      n .    ← next, repeat

Why is . sometimes better than :%s? Because :%s/old/new/g is blind — it applies everywhere, including where it should not — while n + . gives case-by-case control at nearly the same speed. Rule of thumb: more than ~15 occurrences with a precise pattern → :%s with the confirm flag; fewer, or with exceptions → n + ..

Undo is a tree, not a stack. Undo a few times, type something new, and the old branch survives:

:earlier 10   ten changes back      :earlier 5m  the file five minutes ago
:earlier 1f   the state at the last write        :later 30s
g-  /  g+     walk the whole undo tree by time         :undolist

:earlier 1f means "put the file back to what I last saved", without losing the way forward.

To keep undo history after vim exits, put set undofile and set undodir=~/.vim/undo// in your vimrc and create the directory. The trailing double slash builds the undo filename from the full path so same-named files don't collide. Read the security section too: those files keep old content on disk.

"What is the difference between `u`, `U` and `Ctrl-r`?"

u undoes the last change and repeats; Ctrl-r is its inverse (redo). U undoes all changes on the last edited line at once — and counts as a change itself, so pressing it again toggles them back.

The senior addition: vim's undo history is a tree, not a linear list, :earlier/:later/g-/g+ navigate it, and set undofile makes it persist across sessions.


8. Insert mode: entering it well, and tricks inside it

Key Meaning
i / a insert before / after the cursor
I / A start of the line's text (after indent) / end of line
o / O new line below / above, then insert
gi insert exactly where you last left insert mode
cw cc C s S delete + insert in one move

To add a semicolon at the end of a line: not $ then a then ;, just A; and Esc.

Ctrl-w    delete the previous word     Ctrl-u  delete back to the insert start
Ctrl-t / Ctrl-d   add / remove one indent level
Ctrl-r a  insert register a       Ctrl-r =  evaluate and insert, e.g. 24*60 → 1440
Ctrl-o    run one normal-mode command, then return to insert
Ctrl-n / Ctrl-p   complete a word from the open buffers
Ctrl-x Ctrl-f     complete a file path     Ctrl-x Ctrl-l  complete a whole line

Take Ctrl-n seriously: with no plugin at all, three letters of applicationEventPublisher plus Ctrl-n is enough. Vim 9.2 also offers fuzzy insert-mode completion via set completeopt+=fuzzy (see :help 'completeopt'; the value does not exist in older versions).

Pasting from the terminal clipboard destroys indentation

If you are in insert mode and paste a code block with the terminal's Ctrl-Shift-V, vim sees it as typed input and autoindent adds indentation to every line — producing a diagonal staircase.

The right way: press "+p in normal mode to paste from the clipboard register; indentation survives. On modern terminals with bracketed paste (Vim 8.0+) the problem usually disappears on its own. The old :set paste / :set nopaste workaround still works but disables autoindent and abbreviations meanwhile, and is no longer recommended. Check whether you even have a clipboard register with vim --version | grep clipboard; minimal server builds usually report -clipboard.


9. Search and substitute

/pattern   search forward        ?pattern  search backward
n          next match             N        previous match
*          search forward for the word under the cursor (whole word)
#          same, backward        g* / g#   same but without word boundaries
:noh       turn off match highlighting

You will use * constantly: cursor on a variable name, press *, cycle with n.

set incsearch    " jump to the first match as you type
set hlsearch     " highlight every match
set ignorecase   " ignore case
set smartcase    " unless you typed an uppercase letter yourself

Together they mean /user finds both user and User, while /User finds only User — exactly what a human brain expects.

vim regex basics

.        any character         *  zero or more of the previous
^  $     start / end of line   \|  alternation
\<  \>   word start / end boundary
\d \w \s digit / word char / whitespace   (\D \W \S are the negations)
[abc]    character class       [^abc]  negated class
\(...\)  group (the backslash is required)   \1 \2  backreferences
\+  \?   one-or-more / zero-or-one          \{2,5}  repetition range
\zs \ze  start / end of what actually counts as the match

The extra backslashes are annoying; the \v ("very magic") prefix moves the syntax close to PCRE:

/\v(get|set)[A-Z]\w+     ← without \v you must write /\(get\|set\)[A-Z]\w\+
Make `\v` a habit

If you are comfortable with regex in Java, grep -E or JavaScript, always start your pattern with \v. One extra character, ten fewer backslashes. Inside a long :%s that is the difference between "it works" and "half an hour debugging a regex".

Substitute: :s

:[range]s/{pattern}/{replacement}/{flags}
range Meaning
(empty) current line only
% the whole file
1,10 / .,$ / .,+5 lines 1–10 / current line to end / current line plus five
'a,'b from mark a to mark b
'<,'> the last visual selection (inserted for you)
/start/,/end/ from the line matching start to the line matching end
flag Meaning
g every match on each line (without it, only the first)
c confirm each match
i / I ignore / respect case regardless of ignorecase
e do not error when there is no match (essential in scripts)
n change nothing, just report the number of matches
:%s/\vlocalhost:8080/api.internal:8443/g     " replace throughout the file
:%s/timeout/duration/gc                       " with per-match confirmation
:%s/\v\s+$//e                                 " strip trailing whitespace
:%s/\v^(\s*)-/\1  -/                          " indent YAML list items by two
:'<,'>s/,/,\r/g                               " turn each comma into a newline
:%s/\v(\w+)\.(\w+)/\2.\1/g                    " swap two parts (backreferences)
:%s/pattern//gn                               " count only, change nothing
:%s//new/g                                    " empty pattern = your last search
:%&&                                          " repeat the last substitute with its flags

With flag c, vim waits on each match for one key: y yes · n no · a all the rest · q stop · l this one and stop · Ctrl-e/Ctrl-y scroll to see context.

Without `%` you only change one line

The biggest :s trap. In sed the command applies to the whole input; in vim, with no range, it applies to the current line only. So the :s/old/new/g you believe fixed the whole file fixed one line, and you happily typed :wq.

Build the habit: whenever you mean the whole file, type the %. And before any large substitute on a real file, count first: :%s/old//gn reports the number of matches. If it surprises you, your pattern is wrong — not the file.

:global — the most powerful command in vim

:g runs an Ex command on every line matching a pattern; :v (or :g!) runs it on every line that does not match.

:g/DEBUG/d                     " delete every line containing DEBUG
:v/ERROR/d                     " delete every line without ERROR (log filtering)
:g/^\s*$/d                     " delete blank lines
:g/TODO/normal A  <-- check    " append a note to every TODO line
:g/^import/sort                " sort the import lines
:g/pattern/t$                  " copy each matching line to the end of the file
:g/pattern/normal @q           " run macro q on every matching line

Combining :g with macros is one of vim's biggest levers.

"You have a 500 MB log file and you only want ERROR lines from the checkout service. What do you do?"

The senior answer starts with a question: "Should I open an editor at all?" For a large file the right tool is a shell pipeline:

grep -E 'ERROR' app.log | grep 'checkout' | tail -200 > /tmp/slice.log
vim /tmp/slice.log

Opening a huge file in vim means reading all of it into memory, building undo structures, and highlighting every line. If you truly must: vim -u NONE -N bigfile.log, then :syntax off, :set noswapfile, :set noundofile. Inside vim, :v/ERROR.*checkout/d does what grep would have done. Bonus points for noting this belongs in the log aggregation and observability layer, not in grep over a server disk.


10. Visual mode: select first, then act

Sometimes you don't know which motion covers exactly the range you want; visual mode lets you select, see it, then apply an operator.

v  charwise     V  linewise     Ctrl-v  blockwise
o  jump to the other end of the selection (to extend from that side)
gv reselect the previous selection

After selecting, every operator you know works: d c y > < = gU gu gq J r ~.

Visual block: column editing

Ctrl-v is what other editors call "multi-cursor". Scenario: comment out ten lines of a .env file.

1. put the cursor on column one of the first line
2. Ctrl-v       ← enter block mode
3. 9j           ← build the vertical block
4. I            ← insert at the start of the block
5. #            ← type it
6. Esc          ← the magic: it is applied to every line

The critical detail: the text does not appear on the other lines until you press Esc. Many people assume it failed.

Key Meaning
I / A insert at the start / end of the block on every line
$A append at the end of each line, even with different lengths
d / x / c delete / change the column block
r{char} fill the entire block with one character
y yank blockwise (it will paste back as a block)
Two ways to do one job, and how to choose

For six lines, Ctrl-v is faster. For 600 scattered lines identified by a pattern, :g/pattern/normal I# or :%s/^/#/ wins. The rule: visual for what you can see, Ex commands for what you can describe.


11. Registers: vim's many clipboards

vim does not have one clipboard; it has dozens. Each is a register, addressed with ":

"ayy   yank the line into register a      "Ayy  append to register a
"ap    paste register a                    :reg  show all registers
Register Contents
"" unnamed — the last yank or delete (what p uses)
"0 the last yank only; deletes never land here
"1"9 history of multi-line deletes; "1 is the newest
"- the last small delete (less than a line)
"_ the black hole — write here to store nothing
"+ / "* the system clipboard / the X11 primary selection
"% / "# current file name / alternate file name
": / "/ / ". last Ex command / last search pattern / last inserted text
"+y  copy to the system clipboard      "+p  paste from it
"_dd delete without clobbering your copy     "0p  paste the last yank
q:   the command history window (editable; Enter runs the line)   q/  same for searches

On a minimal server or container, vim is usually built without X11 support and "+y fails with E353; there, use the terminal's own mouse selection (hold Shift when mouse=a is on) or move the file with scp.

"How do you move text from one file to another without a system clipboard?"

Named registers. In the first file, "ay5j; then Ctrl-^ or :e otherfile; then "ap. The bonus point: registers are shared across buffers in one session, so this always works; they are not shared between two separate vim instances unless you configure viminfo — where it is simpler to :w /tmp/chunk in one and :r /tmp/chunk in the other.


12. Macros: record one edit, replay it a thousand times

A macro records everything you press into a register so you can replay it — vim's strongest tool for repetitive work.

Diagram: the macro record-and-replay loop — نمودار چرخه‌ی ضبط و پخش macro.

flowchart LR
    A[q then a register letter] --> B[perform the edit normally]
    B --> C[q to stop recording]
    C --> D[@a to replay once]
    D --> E[@@ to repeat the last replay]
    D --> F[10@a to replay ten times]
    D --> G[global command with normal @a]

Suppose you have a list of names and want every line to become "alpha",:

qa          ← start recording into register a
I"          ← go to line start, insert, one quote
Esc
A",         ← go to line end, quote and comma
Esc
j           ← move to the next line ← this key is critical
q           ← stop recording
20@a        ← run it twenty more times

Four rules that make a macro reliable:

  1. Start with an absolute motion (0 or ^) so it does not depend on the cursor's position.
  2. End by moving to the next item — usually j or n.
  3. Do not fear a large count. When the macro hits the end of the file or a command fails, vim aborts the remaining repetitions itself, so precise counting is unnecessary.
  4. Prefer :g over counting:g/^\s*log\./normal @a runs only on the lines that matter.

A macro is just a string in a register, so it is editable: paste it into a scratch buffer with "ap, fix the raw keystrokes, put it back with 0"ay$ — or set it directly with :let @a='...'.

Macros are dangerous on non-uniform data

A macro is blind. If line five has a different shape (an extra quote, or it is empty), the macro presses the same keys anyway and produces garbage — and it runs fast enough to wreck 300 lines before you notice.

Three guards: (1) run it on one item first and look; (2) then 10@a and look again; (3) :w before you start so :e! is always the way back. Each repetition is a separate change, so undoing takes several u presses — or one :earlier 1f.

"What is the difference between `.` and a macro, and when do you pick each?"

. repeats the last change — one atomic operation like ciw or dd. A macro repeats a full key sequence: motions, several changes, even Ex commands.

The rule: if the job is one simple change and you can move between occurrences with n or }, . is lighter and more controllable; if it has several steps, you need a macro. The more senior answer: if the job can be described by a pattern, use neither — write a :%s or :g/…/normal, which is declarative, stays in the history, can be reviewed before running, and can even run from a script with vim -es.


13. Marks and jumps: spatial memory

ma   mark the current spot as local mark a (a-z, within this file)
mA   global mark A (works across files)
`a   return to that exact line and column        'a  to the start of that line
:marks   list all marks

Automatic marks vim maintains for you: where you were before the last jump · . `` the last change · `` ^ where you last left insert mode · " `` where you were last time this file was open · `` [ `] the last yank or paste · '< '> `` the last visual selection.

Ctrl-o   go back in the jumplist
Ctrl-i   go forward in the jumplist (this is Tab)
g;  g,   move through the changelist (previous / next change)
:jumps   list the jumps

Think of Ctrl-o as the browser back button: after a gg, a /pattern or a jump to a definition it returns you where you were. To return and keep typing where you left off, gi does it in one keystroke.


14. Buffers, windows, tabs: getting the mental model right

"Tab" means something different in vim than in a browser or an IDE:

  • Buffer = a file loaded into memory; not necessarily visible.
  • Window (split) = a viewport onto a buffer. Several windows can show the same buffer.
  • Tab page = a layout of windows; a workspace, not a file.

Diagram: buffers vs windows vs tab pages — نمودار رابطه‌ی buffer و window و tab page.

flowchart TD
    F1[(file on disk: App.java)] --> B1[buffer 1]
    F2[(file on disk: app.yml)] --> B2[buffer 2]
    B1 --> W1[window A]
    B1 --> W2[window B]
    B2 --> W3[window C]
    W1 --> T1[tab page 1]
    W2 --> T1
    W3 --> T2[tab page 2]
Concept What it is Use it for
Buffer a file in memory every file open this session — your real file list
Window a view onto a buffer comparing two files, or two parts of one file
Tab page a layout of windows separating independent tasks ("code" vs "tests")
A vim tab is not a replacement for a buffer

The classic mistake for IDE migrants is one tab per file; after fifteen files the tab line is unusable. The vim way: keep all files open as buffers and hop between them with :b partialname or Ctrl-^. Use tab pages only to separate working contexts, not files.

:e path/to/file    open a file             :ls  or  :buffers   list buffers
:b 3               go to buffer 3          :b confi  go by partial name match
:bn  :bp  :bd      next / previous / delete
Ctrl-^  (Ctrl-6)   toggle between the last two buffers — the daily workhorse

By default vim refuses to leave a modified buffer (E37); set hidden removes that restriction and nearly every serious user has it on. Remember :ls marks unsaved buffers with + and :wa writes them all.

Splits:

:sp [file]  horizontal    :vs [file]  vertical    (or Ctrl-w s / Ctrl-w v)
Ctrl-w h j k l   move to the left / lower / upper / right window
Ctrl-w H J K L   move the window itself to an edge
Ctrl-w =  equalise sizes       Ctrl-w _  and  Ctrl-w |  maximise
Ctrl-w c  close this window (the buffer stays)   Ctrl-w o  keep only this one
Ctrl-w T  move this window into a new tab page

Tab pages: :tabnew [file] · gt / gT next and previous · 2gt go to tab two · :tabclose · :tabonly.

Finding files without a plugin

With set path+=** and set wildmenu, :find UserServ<Tab> locates a file anywhere in the project tree and :b User<Tab> searches the open buffers. To search content:

:vimgrep /\vTODO|FIXME/ **/*.java
:copen           " the quickfix window with the result list
:cnext  :cprev   " go to the next / previous result
set grepprg=rg\ --vimgrep\ --smart-case   " with ripgrep installed, :grep is far faster

** covers every subdirectory including node_modules and target/, so exclude those via wildignore (see the starter vimrc) or :find will freeze on a monorepo.

netrw: the built-in file browser

netrw ships with vim and needs no installation: :Ex (or :Explore) browses the current file's directory, and :Sex / :Vex / :Lex do the same in a horizontal split, a vertical split, or a left side panel.

Key Action
Enter / - open file or enter directory / go up one directory
% / d / D / R new file / new directory / delete / rename
v / o / t open in a vertical split / horizontal split / tab
i / I / Ctrl-l cycle listing style / toggle banner / refresh
mf then mc or mm mark files, then copy or move them
netrw and remote paths — a real CVE

netrw can also open remote files (vim scp://host//path/file), building shell commands behind the scenes to do it. That is exactly where a command-injection vulnerability (CVE-2026-28417) was reported in February 2026: hostname validation was not strict enough, so a crafted URL could smuggle shell metacharacters through and run arbitrary commands as the vim user. It was fixed in patch 9.2.0073 with strict RFC1123 validation and shellescape() on host and port.

Two lessons: keep vim patched on servers, and never blindly open an unknown URL in your editor — the same rule you apply to unknown links.

"What exactly does `:g/pattern/normal @q` do, and why is it better than replaying the macro by hand?"

:g works in two passes: it first scans the range and marks matching lines, then runs the command on them. normal @q replays the macro in register q on each marked line.

Why it is better: (1) the target set is fixed before any edit happens, so your changes cannot shift the line list — the thing that makes hand-repeated @q skip lines; (2) unrelated lines are never touched, reducing the risk of a blind macro; (3) the whole operation is one reviewable command that stays in the history.


15. Settings and a .vimrc explained line by line

vim works without config, but ten lines of settings move it from "tolerable" to "pleasant".

Where does vim look for its config? It checks several places in order and the first one found wins. If none exists, $VIMRUNTIME/defaults.vim is loaded, which already gives sensible modern behaviour (nocompatible, incsearch, syntax on, filetype plugin indent on, scrolloff=5).

Diagram: vimrc lookup order at startup — نمودار ترتیب پیدا کردن فایل تنظیمات هنگام راه‌اندازی.

flowchart TD
    A[vim starts] --> B[system vimrc is read first]
    B --> C{VIMINIT env var set}
    C -- yes --> Z[first match wins, stop searching]
    C -- no --> D{HOME/.vimrc exists}
    D -- yes --> Z
    D -- no --> E{HOME/.vim/vimrc exists}
    E -- yes --> Z
    E -- no --> F{XDG_CONFIG_HOME/vim/vimrc exists}
    F -- yes --> Z
    F -- no --> G[load defaults.vim from VIMRUNTIME]
The XDG path in modern Vim

Since patch 9.1.0327 (so also in Vim 9.2), vim on Unix reads $XDG_CONFIG_HOME/vim/vimrc — by default ~/.config/vim/vimrc — but only if ~/.vimrc and ~/.vim/vimrc do not exist, so migrating means moving the old files away. When that path is used, runtimepath and packpath follow it. Run :echo $MYVIMRC to see which file was really loaded — the fastest way to debug "why aren't my settings applied?".

A starter .vimrc where every line has a reason:

" ---------- basics ----------
set nocompatible          " modern vim behaviour, not ancient vi emulation
filetype plugin indent on " filetype detection plus its plugins and indent rules
syntax on                 " syntax highlighting
set encoding=utf-8

" ---------- display ----------
set number relativenumber " absolute number on the cursor line, relative elsewhere
set laststatus=2          " always show the status line
set scrolloff=5           " keep 5 lines of context above and below
set showcmd wildmenu      " show the partial command plus a completion menu

" ---------- search ----------
set incsearch hlsearch
set ignorecase smartcase

" ---------- indentation ----------
set expandtab             " turn Tab into spaces
set tabstop=4             " how wide an existing Tab character looks
set softtabstop=4         " how many spaces pressing Tab inserts
set shiftwidth=4          " one indent level for >> and <<
set autoindent smartindent

" ---------- file behaviour ----------
set hidden                " allow leaving a modified buffer without writing
set autoread              " reload if the file changed outside vim
set undofile
set undodir=~/.vim/undo//   " persist undo history across sessions
set directory=~/.vim/swap//

" ---------- navigation ----------
set path+=**              " recursive :find across the project tree
set wildignore+=**/node_modules/**,**/target/**,**/.git/**,**/build/**
set splitbelow splitright " new splits open below and to the right
set mouse=a               " enable the mouse (resizing splits, scrolling)

" ---------- security ----------
set nomodeline            " do not honour settings embedded inside files

Run mkdir -p ~/.vim/{undo,swap} first, or vim will error when it writes.

Three commands for debugging config:

:so %                     " reload this vimrc without restarting
:set shiftwidth?          " show an option's current value
:verbose set shiftwidth?  " and which file and line set it

:verbose set X? is gold: when a plugin overrides your settings, it names the culprit.


16. Plugins, briefly and with judgment

Since Vim 8 there is a built-in package manager, so simple plugins need no external tooling:

mkdir -p ~/.vim/pack/mine/start          # loaded automatically at startup
mkdir -p ~/.vim/pack/mine/opt            # loaded on demand with :packadd
git clone https://github.com/tpope/vim-fugitive ~/.vim/pack/mine/start/fugitive
vim -c "helptags ALL" -c q          # index the new plugin's help files

With many plugins, vim-plug is the common lightweight choice:

curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
  https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
call plug#begin('~/.vim/plugged')
Plug 'tpope/vim-surround'      " change the quotes/brackets around some text
Plug 'tpope/vim-commentary'    " gcc to comment a line
Plug 'tpope/vim-fugitive'      " git inside vim
call plug#end()

Then inside vim: :PlugInstall, :PlugUpdate, :PlugClean (remove plugins dropped from the config) and :PlugUpgrade (update vim-plug itself).

Approach Benefit Cost Best for
No plugins identical everywhere, zero maintenance and startup cost you must know :find, :vimgrep, Ctrl-n servers, containers, throwaway machines
Built-in packages (pack/*/start) no external dependency, versionable with git manual updates a small, stable plugin set
vim-plug parallel installs, lazy loading, simple commands one external dependency a main workstation with dozens of plugins
Your personal config does not exist on the server

The trap for people who make vim "heavy": they grow so dependent on <leader> mappings and plugins that they are paralysed on a bare server, where there is no .vimrc, no plugins and often no clipboard.

The senior rule: your baseline skill must work under vim -u NONE. Add plugins for comfort, not to compensate for not knowing the tool. Measure startup occasionally: vim --startuptime /tmp/start.log +q then sort -k2 -nr /tmp/start.log | head.


17. vim inside git

git opens an editor for commit messages, interactive rebases, git add -p with e, and git tag -a. Which editor? The precedence is well defined:

Diagram: how git resolves which editor to launch — نمودار انتخاب ادیتور توسط git.

flowchart TD
    A[git needs an editor] --> B{GIT_EDITOR env var}
    B -- set --> Z[launch it]
    B -- not set --> C{core.editor in git config}
    C -- set --> Z
    C -- not set --> D{VISUAL env var}
    D -- set --> Z
    D -- not set --> E{EDITOR env var}
    E -- set --> Z
    E -- not set --> F[fall back to vi]
git config --global core.editor "vim"
git config --global core.editor "vim -f"   # -f means do not fork; needed for GUI editors
export EDITOR=vim                          # other tools read this too
export KUBE_EDITOR=vim                     # kubectl checks KUBE_EDITOR, then EDITOR

kubectl edit follows the same shape: KUBE_EDITOR, then EDITOR, then vi on Linux (notepad on Windows). crontab -e and visudo honour VISUAL/EDITOR.

Writing a good commit message in vim

Write a short imperative summary on line one, leave a blank line, type the body, reflow it with gqap (the convention is 72 columns), then ZZ.

The most important trick: :cq means "exit with an error code". git reads that as "the user gave up" and cancels the commit or rebase — more decisive than deleting the text, and inside a git rebase -i it is the difference between "cancel" and "execute a half-edited plan".

diff and merge

git config --global diff.tool vimdiff
git config --global merge.tool vimdiff
git difftool HEAD~1 -- src/App.java     # compare inside vim
git mergetool                            # resolve conflicts in the four-window layout

git mergetool --tool=vimdiff opens four windows: LOCAL · BASE · REMOTE across the top and MERGED below. The layout is configurable through mergetool.vimdiff.layout, where + opens a new tab and , a vertical split.

Diff-mode keys:

]c   /  [c      jump to the next / previous change
do  (diff obtain)   pull the change from the other window
dp  (diff put)      push the change to the other window
:diffget LOCAL      in a four-way merge: take from a named buffer
:diffput            send to the other buffer
:diffupdate         recompute the diff after manual edits

Vim 9.2 improved diff mode: the linematch algorithm aligns changes using similar lines, far more readable on messy conflicts:

set diffopt+=linematch:60
"You are inside `git rebase -i` and the editor just opened. How do you squash a commit, and how do you abort the whole thing safely?"

In the todo file I change pick in front of the second commit to squash (or s): cursor on the word, cw, type squash, Esc. For several, n then ., or a range-limited :s. Then ZZ.

To abort safely: :cq. It exits non-zero, so git stops the whole rebase without rewriting a single commit. If the rebase already started, git rebase --abort restores the previous state. The weaker answer is "I delete all the lines" — it works, but leaves the intent ambiguous.

"Why do people set `core.editor` to `vim -f`?"

-f means "foreground; do not fork". For terminal vim it is usually a no-op, but for a GUI editor (gvim, code) the process would return immediately, git would think you closed the editor, and it would take the still-empty file as the message — "Aborting commit due to empty commit message", or worse, a truncated one. For code the equivalent is --wait. The question is really about the process model, not about vim.


18. Security and production hygiene

An editor on a server is not harmless. Things that actually matter:

Modelines. vim can read settings from a comment inside the file itself (# vim: set ts=2 sw=2:). That once led to arbitrary command execution: CVE-2019-12735 (fixed in Vim 8.1.1365) let a crafted file run system commands merely by being opened. Modelines are far more restricted today and expression evaluation requires 'modelineexpr', off by default — but where you open unknown files, set nomodeline is a free extra layer.

Open unknown files in a neutral environment.

vim -u NONE -N suspicious.log     # no vimrc, no plugins
vim -Z suspicious.log             # restricted mode: no shell commands
view suspicious.log               # same as vim -R, read-only

Editing root-owned files. The correct pattern is sudoedit (or sudo -e): it opens a temporary copy as your normal user and writes it back as root. With sudo vim you run a full vim as root — one that can :!sh and reads local config with those privileges.

Side files leak data. The swap file (.file.swp), the backup file (file~) and the undo file all keep content on disk. If you edit secrets on a server, disable them for those paths:

autocmd BufNewFile,BufReadPre */secrets/* setlocal noswapfile nobackup noundofile viminfo=
Three mistakes that really happen in production
  1. Editing config directly on a server with no source control. The service works, then a deploy overwrites the file next week and the bug returns. If you must: cp nginx.conf nginx.conf.$(date +%F) first, then apply the same change in the repository.

  2. Leaving vim open on a file and walking away. The swap file stays, the next person gets an E325 warning, presses D, and unsaved work dies.

  3. Running :%s on a file you have not read. Your pattern may match inside a string or a secret. Count first with :%s/pattern//gn, and copy anything sensitive before touching it.

A real example: editing a migration file on a server

Say you must add an index to a migration; the contents differ per database engine:

-- V12__add_index_on_orders.sql
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_customer_created
    ON orders (customer_id, created_at DESC);
ANALYZE orders;

Where does vim come in? For the same index across several tables: select the block with Vjj, y, p a few times, then rename with :%s/\vorders/order_items/g. vim only changes text; whether the migration is correct belongs to sql-mastery and rdbms-tuning.

Never edit a migration that has already run

If this file has been applied anywhere, editing it breaks the migration tool's checksum and the next deploy fails. Create a new file instead. This has nothing to do with the editor — but knowing that "being fast with a tool is not permission to bypass the process" is exactly the judgment expected of a senior.

"You opened a root-owned file as a normal user. How do you save it?"

The standard answer: :w !sudo tee % > /dev/null — pipe the buffer into sudo tee, which writes it to the same file (%) as root, discarding the echoed output; then :e! to reload.

The senior answer continues: the trick has side effects — ownership and ACLs can change, some sudo setups prompt for a password mid-operation and confuse the terminal, and symlinks behave unexpectedly. The right approach is to start with sudoedit /etc/nginx/nginx.conf, which edits a temporary copy safely. Mentioning sudoedit is the difference between "I know a trick" and "I understand the security model".

"How do you run vim with no config and no plugins, and why?"

vim -u NONE -N file. -u NONE reads no vimrc and loads no plugins; -N turns on nocompatible so you keep modern behaviour (without it you land in vi-compatible mode, which lacks multi-level undo).

Three real uses: (1) debugging — the first bisect step when you don't know whether vim or a plugin is at fault; (2) performance on very large files; (3) security, for unknown files, ideally with -Z restricted mode, which blocks shell command execution entirely.


19. When to use nano instead

nano (current stable release: 9.1) is a non-modal editor with its key menu printed at the bottom: Ctrl-O write, Ctrl-X exit, Ctrl-W search, Ctrl-K cut, Ctrl-U paste. If it is installed and your job is a thirty-second change, using it is not weakness — it is picking the right tool.

Criterion vim nano
Availability almost everywhere (at least vi) usually present, not in minimal containers
Learning curve steep; a few days to comfort essentially zero
One-line config change fine fine, and faster
Repetitive refactor across 300 lines macros and :g — seconds effectively impossible
Very large files tunable enough to survive usually very slow
diff/merge inside git integrated (vimdiff) not available
Risk of accidental damage higher (a stray dd) lower
The decision rule

If you know exactly which single change you want and it takes under a minute, whatever editor is at hand is right. If the work is repetitive, pattern-shaped or exploratory, vim gives real leverage. And for large files, the best editor is no editor: keep grep, sed, awk, jq and yq in the same toolbox.


20. Practice drills

The best first investment: run vimtutor in a terminal — an interactive ~30-minute tutorial that ships with vim and works on a temporary copy, so nothing is at risk.

Run these drills on a scratch file, repeating until you stop thinking about them:

  1. Survival: type a few characters, quit without saving; then save and quit, once with :wq and once with ZZ.
  2. Movement: navigate a source file using only gg, G, }, {, Ctrl-d, Ctrl-u, f, ;. Ban the arrow keys.
  3. Grammar: on a function, run ciw, ci", ci(, dap, yi{, >ap, gUiw, pressing u after each.
  4. The dot: find a word in five places with * and change each with ciw + n + ..
  5. Substitute: write a :%s with the c flag, then rerun it with the n flag to count only.
  6. Block: comment ten lines with Ctrl-v + I + #, then uncomment with Ctrl-v + x.
  7. Macro: turn a 20-line list of names into a JSON array; @a once, then 19@a.
  8. Registers and navigation: put five lines into register b, paste them into another file, then bounce between spots with ma, `a and Ctrl-o.
  9. git: commit without -m, reflow the message with gqap, then repeat and abort with :cq.
  10. Getting help: open :help motion.txt, follow a tag with Ctrl-], come back with Ctrl-t. Navigating :help matters more than memorising any single command.

21. Cheat sheet: the commands that cover 95% of real use

Group Commands
Quitting :w write · :q quit · :wq / ZZ save+quit · :q! / ZQ discard · :x write if changed · :qa! all
Modes Esc / Ctrl-[ normal · i a insert · I A line start/end · o O new line · v V Ctrl-v visual · R replace
Line motion h j k l · 0 column zero · ^ first non-blank · $ end of line · w b e words · W B E big words
File motion gg top · G bottom · 42G line 42 · { } paragraphs · % matching bracket · Ctrl-d Ctrl-u half page · zz centre
Targeting f{c} to a char · t{c} before it · F T backwards · ; , repeat
Delete & change x char · dd line · D to line end · dw word · cw change word · cc change line · C change to line end · s r char
Copy & paste yy line · yw word · p after · P before · "0p last yank · "_dd delete unstored · "+y clipboard
Text objects iw aw word · i" a" quotes · i( a( parens · i{ a{ braces · ip ap paragraph · it at tag
Repeat & undo . repeat change · u undo · Ctrl-r redo · U whole line · :earlier 1f back to last write
Search /x ?x · n N · * # word under cursor · :noh clear highlight
Substitute :%s/a/b/g file-wide · :%s/a/b/gc confirm · :%s/a//gn count · :g/x/d delete matching · :v/x/d keep matching
Visual > < indent · = reformat · gU gu case · J join · gv reselect
Registers & macros "ayy yank into a · :reg list · qaq record · @a replay · @@ repeat · 10@a ten times
Marks & jumps ma set · `a jump · Ctrl-o Ctrl-i jump history · g; previous change · % matching pair
Files & windows :e f open · :ls list · :b n switch · Ctrl-^ toggle · :sp :vs split · Ctrl-w hjkl move · gt next tab
Project :find x with path+=** · :vimgrep /x/ **/*.java · :copen :cnext · :Ex browser
Diff ]c [c next/previous · do obtain · dp put · :diffupdate
Shell :!cmd run · :r !cmd insert output · :%!sort filter · Ctrl-z then fg
Help :h {topic} · Ctrl-] follow tag · Ctrl-t back · :h index full key list
Wrapping up

vim is not a pile of strange shortcuts; it is a small language for editing text with three layers. The survival layer — Esc, :q!, :wq — gets you off any server and costs an hour. The fluency layer — motions, the operator + motion grammar, text objects, . and u — makes terminal editing no slower than an IDE and costs a few weeks. The leverage layer — :s with ranges and flags, :g, registers, macros — is where vim does things that are not merely easier elsewhere, but not possible.

The senior judgment being tested is not in the commands: knowing when not to open an editor at all, when nano is enough, why sudoedit beats sudo vim, why your skill must survive vim -u NONE, and why hand-editing a production config creates technical debt. Learn the tool, but do not route around the process.