After years of using VS Code for PHP development, I decided to explore Neovim as a lightweight alternative that could handle my Laravel and Drupal workflows. The transition wasn't just about trying something new—it was about building a setup that matched my daily needs while gaining the speed and efficiency that Neovim promises.
Here's how I configured Neovim to work seamlessly with modern PHP development, specifically optimized for Laravel and Drupal projects on macOS with iTerm2.
Why Neovim for PHP Development?
Before diving into the setup, let me address the obvious question: why switch from VS Code? For me, it came down to a few key reasons:
- Speed: Neovim launches instantly and handles large codebases without lag
- Terminal integration: Since I'm already living in iTerm2, having my editor there feels natural
- Customization: Complete control over every aspect of the editor
- Resource efficiency: VS Code can be a memory hog with multiple projects open
That said, I wanted to replicate the VS Code features I relied on most: file tree navigation, fuzzy finding, syntax highlighting for Blade and Twig templates, and intuitive tab management.
Prerequisites
Before starting, make sure you have these installed:
# Install Neovim
brew install neovim
# Install ripgrep (required for text searching)
brew install ripgrep
# Install a Nerd Font for proper icons
brew install --cask font-hack-nerd-fontSet your iTerm2 font to "Hack Nerd Font" in Preferences → Profiles → Text.
The Foundation: Lazy.nvim Plugin Manager
I chose lazy.nvim as my plugin manager for its speed and simple configuration. Unlike older plugin managers like Packer or vim-plug, lazy.nvim loads plugins on-demand, which keeps Neovim's startup time lightning-fast even with dozens of plugins installed.
Create your Neovim config directory:
mkdir -p ~/.config/nvimCreate ~/.config/nvim/init.lua and start with the plugin manager bootstrap:
-- === PLUGIN MANAGER BOOTSTRAP ===
-- This checks if lazy.nvim is installed, and if not, clones it from GitHub
-- This means your config is portable - lazy.nvim installs itself automatically
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable",
lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
-- === BASIC SETTINGS ===
-- Line numbers without relative numbering (easier to read for most people)
vim.opt.number = true
vim.opt.relativenumber = false
-- Enable mouse support for clicking, scrolling, and selecting
vim.opt.mouse = "a"
-- Sync Neovim clipboard with system clipboard (Cmd+C/Cmd+V work normally)
vim.opt.clipboard = "unnamedplus"
-- Smart case-insensitive search (lowercase = ignore case, uppercase = match case)
vim.opt.ignorecase = true
vim.opt.smartcase = true
-- Indentation settings (2 spaces, matching most PHP/JS style guides)
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
-- Enable 24-bit RGB colors (makes themes look much better)
vim.opt.termguicolors = true
-- iTerm2-specific mouse compatibility settings
-- These ensure clicking in the file tree and buffers works reliably
vim.opt.mousemodel = "popup_setpos" -- Where clicks position the cursor
vim.opt.mousetime = 500 -- How long to wait for double-click
vim.opt.ttimeoutlen = 10 -- Faster escape key response
vim.opt.timeoutlen = 500 -- How long to wait for key combinationsEssential Plugins for PHP Development
Here's the complete plugin configuration that transforms Neovim into a PHP powerhouse. Each plugin serves a specific purpose in replicating and improving upon the VS Code experience:
require("lazy").setup({
-- Syntax highlighting with Treesitter
-- Treesitter provides much better syntax highlighting than traditional regex-based
-- methods. It understands code structure, not just patterns.
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate", -- Automatically update parsers after install
config = function()
require('nvim-treesitter.configs').setup({
-- Install parsers for languages you use
-- These provide syntax highlighting, indentation, and code folding
ensure_installed = {
"php", -- PHP core syntax
"javascript", -- JS in Blade/Twig templates
"typescript", -- TS if you use it
"html", -- HTML in templates
"css", -- CSS/SCSS
"bash", -- Shell scripts
"lua", -- For editing your Neovim config
"json", -- Config files
"yaml", -- Docker, CI configs
"markdown", -- Documentation
},
highlight = {
enable = true,
-- Disable vim's regex highlighting (Treesitter is better)
additional_vim_regex_highlighting = false,
},
indent = { enable = true }, -- Smart indentation based on syntax
})
end
},
-- Laravel Blade syntax highlighting
-- Blade templates have special directives like @if, @foreach that need custom highlighting
{ "jwalton512/vim-blade" },
-- Drupal Twig syntax highlighting
-- Twig uses {{ }}, {% %}, and {# #} syntax that needs special handling
{ "nelsyeung/twig.vim" },
-- File tree explorer (like VS Code's sidebar)
-- This gives you a visual file browser you can click through
{
"nvim-tree/nvim-tree.lua",
dependencies = {
"nvim-tree/nvim-web-devicons" -- Pretty file icons (requires Nerd Font)
},
config = function()
require("nvim-tree").setup({
view = {
width = 35, -- Sidebar width in columns
side = "left", -- Could be "right" if you prefer
},
renderer = {
icons = {
show = {
file = true, -- Show file type icons
folder = true, -- Show folder icons
folder_arrow = true, -- Show expand/collapse arrows
git = true, -- Show git status icons
},
},
},
filters = {
dotfiles = false, -- Show hidden files by default
-- Ignore these directories (they clutter the tree)
custom = { "node_modules", ".git" },
},
})
end
},
-- Fuzzy finder (like Ctrl+P in VS Code)
-- This is THE killer feature - instantly find files across your entire project
{
"nvim-telescope/telescope.nvim",
dependencies = {
"nvim-lua/plenary.nvim" -- Utility library Telescope needs
},
config = function()
require("telescope").setup({
defaults = {
-- Ignore these patterns when searching (speeds up search)
file_ignore_patterns = {
"node_modules", -- JS dependencies
"vendor", -- PHP dependencies
".git", -- Git internals
"storage/framework", -- Laravel cache
"bootstrap/cache" -- Laravel compiled files
},
},
})
end
},
-- Tab/buffer management (like VS Code tabs)
-- Barbar gives you clickable tabs at the top of the window
{
"romgrk/barbar.nvim",
dependencies = {
"nvim-tree/nvim-web-devicons", -- File type icons in tabs
"lewis6991/gitsigns.nvim", -- Git status in tab names
},
config = function()
require("barbar").setup({
animation = true, -- Smooth tab transitions
clickable = true, -- Click tabs to switch (requires mouse support)
icons = {
button = '×', -- Close button character
filetype = {
enabled = true -- Show file type icon in tab
},
},
})
end
},
-- Git integration
-- Shows git diff signs in the gutter (like VS Code)
{ "lewis6991/gitsigns.nvim", config = true },
-- Status line (info bar at bottom)
-- Shows current mode, file name, git branch, cursor position, etc.
{
"nvim-lualine/lualine.nvim",
config = function()
require("lualine").setup({
options = {
theme = "gruvbox" -- Matches our color scheme
}
})
end
},
-- Color scheme
-- Gruvbox is easy on the eyes for long coding sessions
{ "morhetz/gruvbox" },
})
-- Set color scheme
vim.cmd([[colorscheme gruvbox]])Key Mappings: The VS Code Experience
The key to making Neovim feel familiar is mapping shortcuts to match VS Code conventions. These mappings are crucial because they allow you to use muscle memory from VS Code while gaining Neovim's performance benefits.
-- === KEY MAPPINGS ===
-- Set leader key to Space
-- The leader key is like a namespace for custom shortcuts
-- Space is easy to reach and doesn't conflict with default Vim keys
vim.g.mapleader = " "
-- File tree toggle (Ctrl+B like VS Code sidebar)
-- This shows/hides the file explorer on the left
-- noremap = don't allow this mapping to be recursive
-- silent = don't show the command in the command line
vim.keymap.set('n', '<C-b>', ':NvimTreeToggle<CR>',
{ noremap = true, silent = true })
-- Fuzzy file finder (Ctrl+P like VS Code)
-- Type part of a filename and instantly jump to it
-- This searches your entire project, respecting .gitignore
vim.keymap.set('n', '<C-p>',
':Telescope find_files<CR>',
{ noremap = true, silent = true })
-- Search in files (Ctrl+F like VS Code search)
-- This searches the content of all files in your project
-- Uses ripgrep under the hood (blazing fast)
vim.keymap.set('n', '<C-f>',
':Telescope live_grep<CR>',
{ noremap = true, silent = true })
-- Tab navigation (Ctrl+Tab / Ctrl+Shift+Tab)
-- Switch between open files just like browser tabs
vim.keymap.set('n', '<C-Tab>', ':BufferNext<CR>',
{ noremap = true, silent = true })
vim.keymap.set('n', '<C-S-Tab>', ':BufferPrevious<CR>',
{ noremap = true, silent = true })
-- Close tab (Ctrl+W)
-- Close the current file without closing Neovim
vim.keymap.set('n', '<C-w>', ':BufferClose<CR>',
{ noremap = true, silent = true })
-- Save file (Ctrl+S)
-- Works in both normal mode and insert mode
-- In insert mode, it saves then returns you to insert mode
vim.keymap.set('n', '<C-s>', ':w<CR>',
{ noremap = true, silent = true })
vim.keymap.set('i', '<C-s>', '<Esc>:w<CR>a',
{ noremap = true, silent = true })
-- Quick escape from insert mode (optional but recommended)
-- Press 'jk' quickly to exit insert mode instead of reaching for Esc
vim.keymap.set('i', 'jk', '<Esc>',
{ noremap = true, silent = true })
-- Window navigation (moving between splits)
-- Use Ctrl+h/j/k/l to move between split windows
vim.keymap.set('n', '<C-h>', '<C-w>h', { noremap = true })
vim.keymap.set('n', '<C-j>', '<C-w>j', { noremap = true })
vim.keymap.set('n', '<C-k>', '<C-w>k', { noremap = true })
vim.keymap.set('n', '<C-l>', '<C-w>l', { noremap = true })Why these specific mappings?
- Ctrl+B: VS Code users instinctively press this for the sidebar
- Ctrl+P: The most-used shortcut in VS Code for file navigation
- Ctrl+F: VS Code's global search that developers rely on constantly
- Ctrl+S: Universal save shortcut across all applications
- Ctrl+Tab: Browser-like tab switching feels natural
- jk for Esc: Controversial but efficient - your fingers never leave home row
iTerm2-Specific Configuration
Getting mouse clicks to work properly in iTerm2 requires some specific settings. This is one of the trickiest parts of the setup, but it's worth the effort because being able to click files in the tree view feels much more natural.
Why is this necessary? Terminal emulators vary in how they handle mouse input. iTerm2 needs explicit configuration to send mouse events to Neovim in a format Neovim understands.
In iTerm2 Preferences:
- Go to Profiles → Terminal
- Check "Report mouse clicks & drags"
This tells iTerm2 to send mouse events to applications running in the terminal - Go to Profiles → Keys
- Set "Left Option key" to Esc+
This allows Option key combinations to work properly with Neovim shortcuts
Then add this to your Neovim config for proper mouse handling in the file tree:
-- Mouse click support in nvim-tree (iTerm2 compatible)
-- This creates an autocmd that runs when the file tree is opened
vim.api.nvim_create_autocmd('FileType', {
pattern = 'NvimTree', -- Only applies to the file tree buffer
callback = function()
local api = require('nvim-tree.api')
-- Helper function to create keymaps with consistent options
local opts = function(desc)
return {
desc = 'nvim-tree: ' .. desc,
buffer = vim.api.nvim_get_current_buf(), -- Only in this buffer
noremap = true, -- Don't allow recursive mapping
silent = true, -- Don't echo the command
nowait = true -- Execute immediately, don't wait for more keys
}
end
-- Single click to open files
-- This is tricky because iTerm2 sends the click, then we need to:
-- 1. Move the cursor to where you clicked
-- 2. Wait a moment for the cursor to move
-- 3. Get the file node under the cursor
-- 4. Open that node
vim.keymap.set('n', '<LeftMouse>', function()
-- First, process the mouse click to move the cursor
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes('<LeftMouse>', true, false, true),
'n',
false
)
-- Wait 10ms for cursor to move, then open the node
vim.defer_fn(function()
local node = api.tree.get_node_under_cursor()
if node then
api.node.open.edit(node) -- Open in current window
end
end, 10) -- 10ms delay is enough for the cursor to update
end, opts('Open with mouse'))
end,
})What this code does:
- Waits for the file tree to open (
FileTypeautocmd forNvimTree) - Intercepts mouse clicks (
<LeftMouse>) - Moves the cursor to where you clicked
- Waits briefly (10ms) for the cursor position to update
- Gets the file/folder under the cursor
- Opens it just like pressing Enter would
Without this code, clicking in the file tree would move the cursor but wouldn't actually open files—you'd still need to press Enter.
Laravel-Specific Optimizations
For Laravel projects, I added some helpful configurations that make common tasks faster. These are quality-of-life improvements that save countless keystrokes.
-- Laravel artisan commands
-- This creates a custom Neovim command `:Artisan` that runs php artisan
-- Usage: :Artisan migrate, :Artisan make:model User, etc.
vim.api.nvim_create_user_command('Artisan', function(opts)
-- opts.args contains everything after :Artisan
vim.cmd('!php artisan ' .. opts.args)
end, {
nargs = '*' -- Accept any number of arguments
})
-- Quick access to common Laravel files
-- These use the leader key (Space) + l (for Laravel) + specific letter
-- Example: Space + l + r opens routes/web.php
-- Open routes file (Leader + lr)
vim.keymap.set('n', '<leader>lr', ':e routes/web.php<CR>')
-- Find controllers with fuzzy search (Leader + lc)
-- Opens Telescope searching only in the Controllers directory
vim.keymap.set('n', '<leader>lc',
':Telescope find_files cwd=app/Http/Controllers<CR>')
-- Find models (Leader + lm)
vim.keymap.set('n', '<leader>lm',
':Telescope find_files cwd=app/Models<CR>')
-- Find views/Blade templates (Leader + lv)
vim.keymap.set('n', '<leader>lv',
':Telescope find_files cwd=resources/views<CR>')
-- Additional helpful Laravel shortcuts:
-- Open .env file quickly (Leader + le)
vim.keymap.set('n', '<leader>le', ':e .env<CR>')
-- Open composer.json (Leader + lp)
vim.keymap.set('n', '<leader>lp', ':e composer.json<CR>')
-- Search for a Blade component (Leader + lb)
vim.keymap.set('n', '<leader>lb',
':Telescope find_files cwd=resources/views/components<CR>')Practical usage examples:
# In Neovim, run artisan commands without leaving your editor
:Artisan migrate:fresh --seed
:Artisan make:controller UserController
:Artisan queue:work
# Quick navigation
Space + lr # Jump to routes file
Space + lc # Type "User" to find UserController
Space + lm # Type "User" to find User model
Space + lv # Type "dashboard" to find dashboard bladeWhy this is useful:
- No context switching: Run artisan commands without opening a separate terminal
- Fast file navigation: Laravel projects can have hundreds of files, these shortcuts let you jump directly to the right directory
- Muscle memory: After a few days,
Space + l + cbecomes automatic for finding controllers
Drupal-Specific Optimizations
For Drupal development, especially with custom modules and themes, these shortcuts streamline your workflow:
-- Drush commands
-- Similar to the Artisan command, this lets you run Drush from within Neovim
-- Usage: :Drush cr, :Drush sql-dump, :Drush uli, etc.
vim.api.nvim_create_user_command('Drush', function(opts)
vim.cmd('!drush ' .. opts.args)
end, {
nargs = '*' -- Accept any arguments
})
-- Quick access to common Drupal directories
-- Drupal projects have a deep directory structure, these shortcuts help
-- Find custom modules (Leader + dm)
-- Most of your custom code lives in web/modules/custom
vim.keymap.set('n', '<leader>dm',
':Telescope find_files cwd=web/modules/custom<CR>')
-- Find custom themes (Leader + dt)
vim.keymap.set('n', '<leader>dt',
':Telescope find_files cwd=web/themes/custom<CR>')
-- Open settings.local.php (Leader + dc)
-- Where local development database credentials live
vim.keymap.set('n', '<leader>dc',
':e web/sites/default/settings.local.php<CR>')
-- Additional Drupal shortcuts:
-- Open main settings.php (Leader + ds)
vim.keymap.set('n', '<leader>ds',
':e web/sites/default/settings.php<CR>')
-- Find contributed modules (Leader + dM)
vim.keymap.set('n', '<leader>dM',
':Telescope find_files cwd=web/modules/contrib<CR>')
-- Find templates in current theme (adjust path to your theme name)
vim.keymap.set('n', '<leader>dT',
':Telescope find_files cwd=web/themes/custom/YOURTHEME/templates<CR>')
-- Search for twig templates across all themes
vim.keymap.set('n', '<leader>dw',
':Telescope live_grep search_dirs={"web/themes"}<CR>')Practical usage examples:
# In Neovim, run drush commands directly
:Drush cr # Clear cache
:Drush uli # Get one-time login link
:Drush sql-dump > backup.sql # Backup database
:Drush config:export # Export configuration
# Quick navigation in Drupal's deep file structure
Space + dm # Type "my_module" to find your custom module
Space + dt # Type "olivero" to find theme files
Space + dc # Instantly open settings.local.php
Space + dw # Search for "node--article" across all templatesWhy this matters for Drupal:
- Deep directory structure: Drupal projects have paths like
web/modules/custom/my_module/src/Controller/, these shortcuts cut through that - Frequent cache clearing:
:Drush crbecomes second nature - Template hunting: Finding which twig file is rendering a component is much faster with
Space + dw - Configuration: Quick access to settings files when switching between local/dev environments
Daily Workflow
Here's how I use this setup in practice:
- Open a project:
cdinto the project directory and runnvim - Navigate files:
Ctrl+Bto toggle the file tree, orCtrl+Pto fuzzy find - Search across files:
Ctrl+Ffor project-wide search - Edit multiple files: Files open in tabs automatically, navigate with
Ctrl+Tab - Git status: Visible in the status line and file tree with gitsigns
- Run commands: Use
:Artisanor:Drushfor framework-specific commands
Performance Comparison
After a month of daily use, here are the noticeable differences from VS Code:
- Startup time: Neovim opens instantly vs. VS Code's 2-3 second load
- Memory usage: ~50MB for Neovim vs. 300-500MB for VS Code
- Large file handling: Neovim handles 10,000+ line files without lag
- Search speed: Telescope with ripgrep is noticeably faster than VS Code search
Gotchas and Solutions
Clipboard not working?
Make sure you have pbcopy and pbpaste accessible, which should be default on macOS.
Icons not showing?
Install a Nerd Font and set it in iTerm2. Regular fonts won't display the special icons.
Mouse clicks not working in iTerm2?
Double-check the iTerm2 settings mentioned above, particularly "Report mouse clicks & drags."
Treesitter syntax highlighting broken?
Run :TSUpdate to update all parsers, or :TSInstall php for specific languages.
Final Thoughts
This setup has transformed my PHP development workflow. While there was definitely a learning curve (especially getting comfortable with Vim motions), the speed and efficiency gains have been worth it. I still keep VS Code installed for tasks like debugging or when I need to share my screen with someone unfamiliar with Vim, but for 60% of my daily coding, Neovim is now my go-to.
The beauty of this configuration is that it's modular. Start with the basics and add plugins as you discover needs. The PHP/Laravel/Drupal-specific plugins make it particularly powerful for our ecosystem, with proper syntax highlighting for Blade templates and Twig files that just works.
If you're considering making the switch, I'd recommend trying it for a week on a side project before diving into production work. The muscle memory takes time to build, but once it clicks, you might find it hard to go back.