Lời nói đầu
Mặc dù sở hữu keymap bá đạo và giúp cho người dùng trở nên vô cùng ảo ma một khi đã thuần thục, bản thân Neovim hay cả Vim đều có giao diện mặc định khá tệ hại, ảnh hưởng rất lớn tới người muốn tiếp cận. Trong nội dung bài này, mình sẽ cùng các bạn setup một môi trường code tốt hơn (rất nhiều), thậm chí có thể đáp ứng daily workflow của các bạn 😊
Cài đặt Neovim
Có nhiều cách để cài đặt Neovim thậm chí ngay từ command line, như sử dụng LaunchPad PPA repo, Snapcraft, Flatpak,... Tuy nhiên phần lớn những bên này đều chưa update phiên bản mới nhất. Tại thời điểm viết bài, Neovim đã ra phiên bản 0.7 được 2 tuần. Và trong bài này mình sẽ cố gắng sử dụng phiên bản mới nhất này để demo, giúp cho cả những bạn tiếp cận sau không bị quá outdated 😂
Đầu tiên mình sẽ về thư mục user home và tải, link tải có ở trên trang github release, các bạn chỉ cần kéo xuống qua phần release note là sẽ thấy list các package, chuột phải vào và copy link là xong.
Do mình dùng Ubuntu nên mình sẽ tải nvim-linux64.deb
cd ~ && wget https://github.com/neovim/neovim/releases/download/v0.7.0/nvim-linux64.deb
sudo dpkg -i nvim-linux64.deb
Sau khi cài đặt thành công, bạn có thể gõ nvim
(không phải neovim
nhé) và nó sẽ đưa bạn vào màn mình start của Neovim. Bản mới nhưng giao diện mặc định vẫn tệ như những phiên bản trước =))
Ok nếu bạn nào đã vào trong rồi thì hãy :q
để thoát, sau đó chúng ta sẽ tạo 1 file config và bắt đầu hành trình tùy biến Neovim
Option
Như bài trước mình đã nói thì Neovim hỗ trợ cả VimScript lẫn Lua, tuy nhiên do trình độ có hạn và mình lỡ tiếp cận Vim bằng VimScript nên trong bài này, các config sẽ viết trên VimScript.
Neovim (và cả Vim) nhận config từ một path cụ thể là ~/.config/nvim/init.vim
(hoặc ~/.config/nvim/init.lua
nếu bạn dùng Lua). Thời điểm hiện tại file này hẳn chưa tồn tại do mình mới cài đặt, và Neovim đang dùng mọi config mặc định. Chúng ta sẽ tạo và mở file này.
install -Dv /dev/null ~/.config/nvim/init.vim
nvim ~/.config/nvim/init.vim
Tiếp theo chúng ta cùng thêm một vài options tăng độ thiện cảm cho người dùng
set number
set relativenumber
set tabstop=4
set softtabstop=4
set shiftwidth=4
set smarttab
set expandtab
set autoindent
set list
set listchars=tab:▸\ ,trail:·
set clipboard=unnamedplus
set encoding=UTF-8 syntax on
filetype plugin on
Giải thích qua một chút thì
number
vàrelativenumber
giúp hiện hàng đánh số thứ tự line dạng relative, bạn có thể nhìn vào và biết tổ hợp hành động của mình sẽ như nào, ví dụ đang ở dòng 5, muốn xóa tới hết dòngset mouse=a
thì ấnd7j
(ấnu
để undo nhé =)))tabstop
,softtabstop
,shiftwidth
,smarttab
,expandtab
,autoindent
nhằm định nghĩa lại một số behavior của nútTab
và indentlist
vàlistchars
giúp chúng ta dễ hình dung những khoảng trống là gìclipboard=unnamedplus
giúp Neovim sử dụng chung bộ nhớ clipboard với hệ thống- Danh sách đầy đủ cũng như giải thích chi tiết có ở đây. https://neovim.io/doc/user/options.html
Sau khi lưu lại :w
, các bạn có thể quit vào lại, hoặc gõ :source %
để nạp lại luôn options mới
Keymap
Keymapping nhằm mục đích tạo shortcut để lặp lại một hoặc một tổ hợp các lệnh. Keymap mặc định của Neovim là tương đối mạnh mẽ, nhưng mình vẫn muốn custom thêm một chút cho phù hợp với thói quen/sở thích bản thân.
Tạo keymap
Cú pháp cơ bản để set một keymap
{context} {attribute?} {input} {result} trong đó {context} - ngữ cảnh mà shortcut này có thể khởi động
{attribute?} - thuộc tính, không bắt buộc, có thể là các giá trị sau: <buffer>, <silent>, <expr> <script>, <unique> and <special>. Có thể có nhiều attribute cùng lúc
{input} - là một hoặc tổ hợp các key bạn muốn ấn để khởi động shortcut này
{result} - là sự phối hợp của các key mặc định hoặc câu lệnh cụ thể để tạo ra kết quả bạn mong muốn
Ví dụ, đang ở trong Insert Mode, mình thấy với tay lên <Esc>
để trở lại Normal Mode
là hơi tốn kalo, nên mình muốn khi đang trong Insert Mode, có thể ấn jj
để thoát, thì mình sẽ khai báo trong file config thêm 1 dòng là
imap jj <esc>
Các loại context
Mặc định khi khai báo map là nó sẽ đệ quy. Giả sử bạn map j
thành k
, và k
thành j
=> khi bạn ấn j
nó sẽ foward gọi thành k
, k
forward gọi thành j
và 💥Để tránh điều này xảy ra thì VimScript có một khai báo nore
- no recursive. Dưới đây là các context khả dụng
Flag | Mode | Cú pháp khả dụng |
---|---|---|
n | Normal mode | nmap nnoremap |
i | Insert mode | imap inoremap |
v | Visual + Select mode | vmap vnoremap |
x | Visual mode | xmap xnoremap |
s | Select mode | smap snoremap |
o | Operator Pending mode | omap onoremap |
! | Insert + Command Line mode | map! noremap! |
Normal + Visual + Operator Pending mode | map noremap |
Một số keymap mình thấy hữu ích
let mapleader = "\<space>" " Quick edit and reload vim config
nmap <leader>ve :edit ~/.config/nvim/init.vim<cr>
nmap <leader>vr :source ~/.config/nvim/init.vim<cr> " Remove all buffers (recent open files)
nmap <leader>Q :bufdo bdelete<cr> " Remove highlight
noremap <silent> <esc> :noh <CR> " Allow gf to open/create non exists file
map gf :edit <cfile><cr> " Maintain the cursor position when yanking a visual selection
" http://ddrscott.github.io/blog/2016/yank-without-jank/
vnoremap y myy`y
vnoremap Y myY`y " Make Y behave like other capitals
nnoremap Y y$ " Quicky escape to normal mode
imap jj <esc> " Save file the traditional way
imap <C-s> <esc> :w <cr>
nmap <C-s> :w <cr> " Search selected text
vnoremap // y/\V<C-R>=escape(@",'/\')<CR><CR>
Chia để trị
Nếu các follow mình thì hiện cái file init.vim
sẽ tựa tựa như này.
Có chút lộn xộn rồi đấy. Mà mình chỉ mới config cho options và keymaps thôi. Còn cả đống plugins cần giới thiệu. Chúng ta sẽ chia nhỏ những config này ra để tiện quản lý nhé.
- Đầu tiên, gõ
ggO
để tạo một dòng trên cùng của file và type vào:
source `~/.config/nvim/options.vim
-
Trở về Normal Mode, ấn
j
để xuống dòng thứ 2, mình sẽ thấy dòngfiletype plugin on
của mình đang được đánh số 14, nên mình sẽ cut phần này bằng lệnhd14j
, rồi ấnk
để quay lại dòng 1. -
ở keymap mình có khai báo
gf
để tạo 1 file chưa tồn tại, mình sẽ để con trỏ vào địa chỉ fileoptions.vim
kia (chỗ nào trên đường dẫn là được) và gõgf
để mở. -
ấn
p
để paste đoạn options,kdd
cho nó xóa dòng bị thừa do dính ký tự xuống dòng, vàctrl s
để lưu -
ấn
:bn
để quay lại fileinit.vim
, ấnyyp
để clone dòng source options.vim đó -
ấn
fp
để đi tới wordoptions
, ấnciw
và sửa thànhkeymaps
rồictrl s
-
tiếp tục navigate tới dòng
let mapleader
, lần này mình sẽ cắt hết đoạn dưới luôn bằng lệnhdG
-
làm tương tự như trên,
k
f/
gf
pkdd
,ctr s
-
nạp lại bằng lệnh
space vr
xem nó còn ok chứ 🤣
Plugin
Trước tiên để cài đặt được plugin trên Neovim thì chúng ta cần sử dụng một trình quản lý plugin. Và trong series này thì mình sẽ sử dụng VimPlug vì nó dễ sử dụng và hỗ trở cả trên Vim lẫn Neovim.
Tại file init.vim
, chúng ta tiếp tục clone 1 dòng nữa xuống dưới cùng và đặt tên là plugins.vim
chẳng hạn, sau đó gf
để mở file
Chi tiết về quá trình cài đặt các bạn có thể tìm trong hướng dẫn của VimPlug mà mình đã dẫn link ở trên. Tuy nhiên, mình mò được một trick trên mạng, giúp tự cài đặt VimPlug, bằng cách copy đoạn sau và dán lên trên cùng
" Automatically install vim-plug
let data_dir = has('nvim') ? stdpath('data') . '/site' : '~/.vim'
if empty(glob(data_dir . '/autoload/plug.vim')) silent execute '!curl -fLo '.data_dir.'/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim' autocmd VimEnter * PlugInstall --sync | source $MYVIMRC
endif call plug#begin(data_dir . '/plugins') call plug#end()
Các bạn chú ý 2 dòng call plug#begin
và call plug#end
, tiếp theo, các package chúng ta muốn cài sẽ cần phải đặt giữa 2 dòng này. Ngoài ra lệnh trên sẽ được thực thi khi sự kiện VimEnter
xảy ra, nên chúng ta cần thoát Neovim và vào lại nhé. Tiếp theo đây chúng ta có thể bắt đầu cài plugin được rồi 😂
Theme
Cùng bắt đầu với customtize cái statusline, ở đây mình sử dụng Lightline là một package giúp hiện trạng thái của con trỏ, như Mode, dòng, cột,...
Để cài đặt một plugin, chúng ta sẽ cần khai báo Plug '<link-github-của-plugin>'
. Có thể khai báo tối giản kiểu itchyny/lightline
nhưng nên gõ đầy đủ để có thể dễ mở link hơn. Đối với cái statusline này chúng ta chỉ cần copy dòng dưới đây và nhét vào giữa 2 cái dòng call plug#
Plug 'https://github.com/itchyny/lightline.vim'
Sau khi khai báo link của plugin, việc cần làm còn lại là :
- Lưu config
ctr s
- Nạp lại config
space vr
- Tải plugin
:PlugInstall
và ấn enter - Nạp lại editor lần nữa
space vr
- Một số plugin có thể sẽ yêu cầu khởi động lại editor để có hiệu lực
Tới lúc này sẽ có một cái status thay cho cái mặc định ở dưới, nhìn qua cũng không tệ 🤣
Tiếp đến là colorscheme. Ở đây mình chọn OneDark Cái này là tùy khẩu vị mỗi người, các bạn có thể tự chọn màu khác trên mạng nhé.
Plug 'https://github.com/navarasu/onedark.nvim'
Tiếp tục làm các bước để cài đặt plugin. Sau đó chúng ta cần define colorscheme cho editor, và việc khai báo này cần nằm dưới call plug#end()
, nếu không lần mở editor sau nó sẽ hú lỗi vì không tìm thấy colorscheme 😰 Vì mình dùng theme onedark
nên mình cũng sẽ khai báo lightline dùng theme one
luôn
colorscheme onedark
let g:lightline = { \ 'colorscheme': 'one' \ }
Chia nhỏ config plugin và Auto command
Một lần nữa, mình sẽ tiếp tục chia nhỏ các plugin và config của những plugin đó vào từng file riêng, tránh tạo ra 1 god file để định nghĩa các plugin. Thoát khỏi Neovim và tạo một file theme.vim
nằm trong sub-directory plugins
install -Dv /dev/null ~/.config/nvim/plugins/theme.vim
Quay trở lại plugins.vim
và move những gì liên quan tới statusline và colorscheme vào trong file theme.vim
vừa tạo
Lúc này nếu bạn nạp lại config thì sẽ không có vấn đề gì, nhưng nếu khởi động lại Neovim thì hẳn sẽ thấy lỗi kiểu như này.
Lý do thì cũng như mình vừa nói ở trên, đoạn source file này đã đưa khai báo colorscheme vào bên trong call plug#
, chúng ta cần phải tìm cách để đưa nó ra ngoài một lần nữa. Và mình sẽ sử dụng Auto Command. Đầu tiên mình sẽ tạo ra một sự kiện phía dưới call plug#
, nằm trong namespace là User
để nó không tự động thực thi, tên là PlugLoaded
chẳng hạn.
doautocmd User PlugLoaded
Tiếp theo, trong file theme.vim
, mình sẽ khai báo chạy lệnh colorscheme dưới namespace của PlugLoaded
, tức là khi nào PlugLoaded
, thì mới gọi colorscheme onedark
ra.
Thời điểm này nếu khởi động lại Neovim thì sẽ ổn rồi đấy 😍
Tìm kiếm file
Navigate giữa các file trong một project lớn là một điều quan trọng, và thật may mắn khi có Telescope cover phần này. Các package mà Telescope đề nghị gồm:
Chi tiết cách cài đặt có ở link. Mình dùng Ubuntu nên sẽ cài thông qua apt:
sudo apt install ripgrep
sudo apt-get install fd-find
sudo apt-get install fzf
Xong rồi thì quay lại plugins.vim
và thêm một entry nữa, đặt tên là telescope.vim
chẳng hạn
Tiếp tục gf
vào file và điền các package yêu cầu, cũng như keymap đề nghị và setup config cho Telescope. Ngoài ra mình setup thêm tổ hợp ctr p
để find file vì là phím tắt quen thuộc của mình trên VSCode 🤣
Plug 'https://github.com/nvim-telescope/telescope.nvim'
Plug 'https://github.com/nvim-lua/plenary.nvim'
Plug 'https://github.com/nvim-telescope/telescope-fzy-native.nvim'
Plug 'https://github.com/sharkdp/fd' " Find files using Telescope command-line sugar.
nnoremap <C-p> <cmd>Telescope find_files<cr>
nnoremap <leader>ff <cmd>Telescope find_files<cr>
nnoremap <leader>fg <cmd>Telescope live_grep<cr>
nnoremap <leader>fb <cmd>Telescope buffers<cr>
nnoremap <leader>fh <cmd>Telescope help_tags<cr> function SetupTelescope()
lua << EOF
require'telescope'.setup({ defaults = { file_ignore_patterns = { "^./.git/", "^node_modules/", "^vendor/" }, }, pickers = { find_files = { hidden = true } }
})
require'telescope'.load_extension('fzy_native')
EOF
endfunction augroup TelescopeOverrides autocmd! autocmd User PlugLoaded call SetupTelescope()
augroup END
Sau khi install và quite editor, chúng ta cùng trải nghiệm nó một chút chính thư mục config nào bằng cách mở nvim tại thư mục config nhé
z ~/.config/nvim
nvim .
lúc này bạn có thể thử ctrl p
hoặc space ff
để mở tìm kiếm bằng tên file, space fg
để tìm kiếm nội dung file, space fb
để mở buffers coi các file đã mở gần đây. Và rất nhiều shorcut key cho từng màn, các bạn có thể tìm hiểu thêm trên trang chủ telescope.
LSP + Treesitter
Nhằm giúp tăng trải nghiệm code của Neovim, chúng ta sẽ tiếp tục setup các thành phần LSP, popup, treesitter mà mình đã giới thiệu ở bài truớc. Đầu tiên vẫn là tạo 1 thêm 1 file config riêng, mình sẽ đặt là intel.vim
Plug 'https://github.com/junnplus/nvim-lsp-setup'
Plug 'https://github.com/neovim/nvim-lspconfig'
Plug 'https://github.com/williamboman/nvim-lsp-installer'
Plug 'https://github.com/hrsh7th/cmp-nvim-lsp'
Plug 'https://github.com/hrsh7th/cmp-buffer'
Plug 'https://github.com/hrsh7th/cmp-path'
Plug 'https://github.com/hrsh7th/cmp-cmdline'
Plug 'https://github.com/hrsh7th/nvim-cmp'
Plug 'https://github.com/hrsh7th/cmp-vsnip'
Plug 'https://github.com/hrsh7th/vim-vsnip'
Plug 'https://github.com/nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'} function SetupTreesitter()
lua << EOF
require'nvim-treesitter.configs'.setup { ensure_installed = { "lua", "php", "html", }
}
EOF
endfunction function SetupLsp()
lua << EOF
require('nvim-lsp-setup').setup({ mappings = { gf = 'lua vim.lsp.buf.formatting()', gd = 'lua require"telescope.builtin".lsp_definitions()', gi = 'lua require"telescope.builtin".lsp_implementations()', gr = 'lua require"telescope.builtin".lsp_references()', }, servers = { intelephense = {}, },
})
EOF
endfunction function SetupCompletion()
lua <<EOF local cmp = require'cmp' cmp.setup({ snippet = { expand = function(args) vim.fn["vsnip#anonymous"](args.body) -- For `vsnip` users. end, }, mapping = cmp.mapping.preset.insert({ ['<C-b>'] = cmp.mapping.scroll_docs(-4), ['<C-f>'] = cmp.mapping.scroll_docs(4), ['<C-Space>'] = cmp.mapping.complete(), ['<C-e>'] = cmp.mapping.abort(), ['<CR>'] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items. }), sources = cmp.config.sources({ { name = 'nvim_lsp' }, { name = 'vsnip' }, -- For vsnip users. }, { { name = 'buffer' }, }) }) -- Use buffer source for `/` (if you enabled `native_menu`, this won't work anymore). cmp.setup.cmdline('/', { mapping = cmp.mapping.preset.cmdline(), sources = { { name = 'buffer' } } }) -- Use cmdline & path source for ':' (if you enabled `native_menu`, this won't work anymore). cmp.setup.cmdline(':', { mapping = cmp.mapping.preset.cmdline(), sources = cmp.config.sources({ { name = 'path' } }, { { name = 'cmdline' } }) }) EOF
endfunction augroup LspOverrides autocmd! autocmd User PlugLoaded call SetupTreesitter() autocmd User PlugLoaded call SetupLsp() autocmd User PlugLoaded call SetupCompletion()
augroup END
Đây là config mẫu cho project php cơ bản, mà mình đã sử dụng intelephense. Các bạn sẽ thấy mình đã chia việc setup này làm 3 hàm cho dễ hình dung
- ở
SetupTreesitter()
thì các bạn cần chú ý thêm các ngôn ngữ bạn muốn vào trong objectensure_installed
, hoặc replace object đó với"all"
để cài đặt (cỡ 160 ngôn ngữ gì đó` - ở
SetupLsp()
, mình dùng package nvim-lsp-setup để nó cover hầu hết phần khó cho mình, còn lại mình chỉ khai báo thêm keymap tích hợp telescope và LSP mà mình dùng, cụ thể là intelephense. Coi default keymap ở document trên cái link trên. Ngoài ra các bạn cần chú ý các requirement của LSP tại https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md. Ví dụ mình dùngintelephense
thì cần cài đặt npm packageintelephense
ở mức global (npm i -g intelephense
) và thêm dòngintelephense = {},
bên trong objectservers
- đối với
SetupCompletetion()
, các bạn chỉ cần để ý setup keymap là được 😊
Và các plugin khác
Sau khi hoàn thành cài đặt như trên, Neovim của chúng ta đã tương đối ra dáng một IDE rồi 😊 Và bạn hẳn cũng đã hình dung ra cách cài đặt plugins. Ở phần này mình sẽ liệt kê các plugin khác mà mình dùng cho daily workflow để các bạn có thể tìm hiểu và tự cài đặt
- Coc Explorer - là một phần mở rộng của Coc, plugin này cho bạn một UI của explore cực kỳ đẹp mắt, đầy đủ chức năng cho việc navigate + manipulate file/directory
- Github Copilot - trợ lý ảo suggest code cực bá đạo
- EasyMotion - navigate trong screen, vô cùng nhanh và dễ dàng.
- Floaterm - mở 1 popup terminal ngay trong editor và sử dụng
- NerdCommeter - thêm một action
gc
để comment. ví dụgcc
để comment dòng hiện tại,gc4j
để comment dòng hiện tại và 4 dòng phía dưới. - Surround - thêm một count
s
để làm việc với xung quanh tương tự như tổ hợp với inside/around. Ví dụ muốn đổi"hello"
thành'hello'
, ta chỉ cần sử dungjcs"'
- Neoformat - format sử dụng prettier
- Sayonara - tắt file và xóa khỏi buffers (recent files)
- NeoScroll - scroll mượt hơn
- WordMotion - phân tách các word được viết bằng
camelCase
hoặcsnake_case
Và còn nhiều nữa...
Tổng kết
Chúng ta đã vừa điểm qua cách mình biến Neovim thành một code editor có tương đối đấy đủ các chức năng. Hi vọng qua bài này, các bạn làm quen với cách cài đặt cũng như sử dụng Vim thông qua các ví dụ.
Ngoài ra các bạn có thể tham khảo phiên bản đầy đủ toàn bộ dotfiles của mình. Dưới đây là preview Neovim mà mình xài, cùng một spotlight với ảnh trên
Hẹn gặp lại các bạn trong những bài viết tiếp theo của series.