Enhanced Neovim Setup for Flutter Development on macOS
Minimal setup:

Final config with a lot more features can be found here:

This setup gives you a clean, isolated Neovim environment built specifically for Flutter development. It keeps your main Neovim or Vim configs untouched, loads fast, looks good, and comes with debugging, autocompletion, Treesitter, Flutter tools, commenting, LSP UI, and everything else a sane developer would want.
Launch it using:
NVIM_APPNAME=nvim-flutter nvim
Because mixing configs is a crime.
Why Use an Isolated Neovim Setup?
- Zero clashes with your normal Neovim or Vim config
- Perfect for language-specific setups
- Easy to back up, update, or delete without ruining anything else
- Lets you experiment like a gremlin without consequences
Your config lives in:
~/.config/nvim-flutter/
Installation Steps
1. Install Dependencies
brew install neovim
brew install --cask flutter
Make sure:
flutter doctor
is not crying.
2. Create the Isolated Config Folder
mkdir -p ~/.config/nvim-flutter
Put your init.lua (the full code you pasted above) inside that folder.
3. Full init.lua Configuration
-- Neovim Configuration for Flutter Development
-- =============================================
-- Leader key setup
vim.g.mapleader = " "
vim.g.maplocalleader = " "
-- Bootstrap lazy.nvim plugin manager
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)
-- Plugin Configuration
-- ====================
require("lazy").setup({
-- LSP Base
{"neovim/nvim-lspconfig"}, -- Dart Syntax Support
{"dart-lang/dart-vim-plugin"}, -- Debugging Tools
{"mfussenegger/nvim-dap"}, {
"rcarriga/nvim-dap-ui",
dependencies = {"mfussenegger/nvim-dap", "nvim-neotest/nvim-nio"},
config = function()
require("dapui").setup({
layouts = {
{
elements = {
{id = "scopes", size = 0.25},
{id = "breakpoints", size = 0.25},
{id = "stacks", size = 0.25},
{id = "watches", size = 0.25}
},
position = "left",
size = 40
}, {
elements = {
{id = "repl", size = 0.5},
{id = "console", size = 0.5}
},
position = "bottom",
size = 10
}
},
icons = {
expanded = "▾",
collapsed = "▸",
current_frame = "▸"
}
})
-- Auto-open/close DAP UI on debug session start/end
local dap, dapui = require("dap"), require("dapui")
dap.listeners.after.event_initialized["dapui_config"] = function()
dapui.open()
end
dap.listeners.before.event_terminated["dapui_config"] = function()
dapui.close()
end
dap.listeners.before.event_exited["dapui_config"] = function()
dapui.close()
end
end
}, {
"theHamsta/nvim-dap-virtual-text",
dependencies = {
"mfussenegger/nvim-dap", "nvim-treesitter/nvim-treesitter"
},
config = function()
require("nvim-dap-virtual-text").setup({
enabled = true,
enable_commands = true,
highlight_changed_variables = true,
highlight_new_as_changed = false,
show_stop_reason = true,
commented = false,
only_first_definition = true,
all_references = false,
display_callback = function(variable, _buf, _stackframe, _node)
return variable.name .. " = " .. variable.value
end,
virt_text_pos = "eol",
all_frames = false,
virt_lines = false,
virt_text_win_col = nil
})
end
}, -- Flutter Tools Integration
{
"akinsho/flutter-tools.nvim",
lazy = false,
dependencies = {"nvim-lua/plenary.nvim", "stevearc/dressing.nvim"},
config = function()
require("flutter-tools").setup({
ui = {border = "rounded"},
closing_tags = {enabled = true},
debugger = {enabled = true, run_via_dap = true},
dev_log = {enabled = false},
lsp = {
capabilities = require("cmp_nvim_lsp").default_capabilities(),
settings = {showTodos = true, completeFunctionCalls = true}
}
})
-- Flutter keymaps
local map = vim.keymap.set
map("n", "<leader>fr", function()
vim.cmd(
"FlutterRun -d web-server --web-port=8008 --web-hostname=localhost --dart-define=ENVIRONMENT=dev")
end, {desc = "Flutter Run (Web Server)"})
map("n", "<leader>fq", "<cmd>FlutterQuit<CR>",
{desc = "Flutter Quit"})
-- DAP debug keymaps
local dap = require("dap")
map("n", "<leader>db", "<cmd>FlutterDebug<CR>",
{desc = "Start Flutter Debug"})
map("n", "<leader>dc", dap.continue, {desc = "Continue"})
map("n", "<leader>do", dap.step_over, {desc = "Step Over"})
map("n", "<leader>di", dap.step_into, {desc = "Step Into"})
map("n", "<leader>du", dap.step_out, {desc = "Step Out"})
map("n", "<leader>dr", dap.restart, {desc = "Restart"})
map("n", "<leader>ds", dap.stop, {desc = "Stop"})
map("n", "<leader>dt", function()
require("dapui").toggle()
end, {desc = "Toggle DAP UI"})
map("n", "<leader>dBp", dap.toggle_breakpoint,
{desc = "Toggle Breakpoint"})
map("n", "<leader>dBP", function()
dap.set_breakpoint(vim.fn.input("Breakpoint condition: "))
end, {desc = "Breakpoint with Condition"})
end
}, -- Tree-sitter (Syntax Highlighting)
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = {"dart", "lua", "vim", "vimdoc"},
highlight = {enable = true},
indent = {enable = true}
})
end
}, -- Completion Engine
{"hrsh7th/nvim-cmp"}, {"hrsh7th/cmp-nvim-lsp"}, {"hrsh7th/cmp-buffer"},
{"hrsh7th/cmp-path"}, -- Commenting
{
"numToStr/Comment.nvim",
config = function()
local comment = require("Comment")
comment.setup({
padding = true,
sticky = true,
ignore = "^$",
mappings = {basic = false, extra = false, extended = false},
pre_hook = function(ctx)
-- Set commentstring based on filetype
if vim.bo.filetype == "dart" then
vim.bo.commentstring = "// %s"
elseif vim.bo.filetype == "lua" then
vim.bo.commentstring = "-- %s"
end
end
})
-- Custom mappings with gC prefix
local api = require("Comment.api")
local map = vim.keymap.set
-- Explicitly remove any default mappings that might conflict
pcall(vim.keymap.del, "n", "gcc")
pcall(vim.keymap.del, "n", "gbc")
pcall(vim.keymap.del, "n", "gc")
pcall(vim.keymap.del, "n", "gb")
pcall(vim.keymap.del, "v", "gc")
pcall(vim.keymap.del, "v", "gb")
-- Line comment: gCc (instead of gcc)
map("n", "gCc", function()
if vim.bo.modifiable then
api.toggle.linewise.current()
else
vim.notify("Buffer is not modifiable", vim.log.levels.WARN)
end
end, {desc = "Comment line"})
-- Block comment: gCb (instead of gbc)
map("n", "gCb", function()
if vim.bo.modifiable then
api.toggle.blockwise.current()
else
vim.notify("Buffer is not modifiable", vim.log.levels.WARN)
end
end, {desc = "Comment block"})
-- Operator/Visual mode: gc (for motions like gCiw, gCap, or visual selections)
map({"n", "v"}, "gc",
api.locked(function() return api.toggle.linewise end),
{desc = "Comment operator/selection"})
-- Block operator/Visual mode: gB (capital B to avoid conflicts)
map({"n", "v"}, "gB",
api.locked(function() return api.toggle.blockwise end),
{desc = "Comment block operator/selection"})
end
}, -- UI Components
{"nvim-lualine/lualine.nvim", config = true},
{"lewis6991/gitsigns.nvim", config = true}, -- Keybinding Helper
{
"folke/which-key.nvim",
event = "VeryLazy",
init = function()
vim.o.timeout = true
vim.o.timeoutlen = 300
end,
config = function()
require("which-key").setup({
plugins = {
marks = true,
registers = true,
spelling = {enabled = false},
presets = {
operators = false,
motions = false,
text_objects = false,
windows = false,
nav = false,
z = false,
g = false
}
},
win = {padding = {1, 2}, wo = {winblend = 0}},
layout = {
height = {min = 4, max = 25},
width = {min = 20, max = 50},
spacing = 3,
align = "left"
},
show_help = true,
show_keys = true
})
end
}, -- File Explorer
{
"nvim-tree/nvim-tree.lua",
dependencies = {"nvim-tree/nvim-web-devicons", "echasnovski/mini.icons"},
config = function()
require("nvim-tree").setup({
view = {width = 35, side = "left"},
filters = {dotfiles = false},
git = {enable = true, ignore = false}
})
-- Auto-open file tree on startup
vim.api.nvim_create_autocmd("VimEnter", {
once = true,
callback = function() vim.cmd("NvimTreeOpen") end
})
end
}, -- Colorscheme
{
"catppuccin/nvim",
name = "catppuccin",
priority = 1000,
config = function()
require("catppuccin").setup({
flavour = "macchiato",
integrations = {
nvimtree = true,
lualine = true,
gitsigns = true,
treesitter = true,
cmp = true
}
})
vim.cmd.colorscheme("catppuccin")
end
}, -- LSP UI (Lspsaga)
{
"glepnir/lspsaga.nvim",
event = "LspAttach",
config = function()
require("lspsaga").setup({
lightbulb = {enable = false},
symbol_in_winbar = {enable = false}
})
end
}
})
-- General Neovim Settings
-- =======================
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.signcolumn = "yes"
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
vim.opt.termguicolors = true
vim.opt.cursorline = true
-- Set commentstring for common file types
vim.api.nvim_create_autocmd("FileType", {
pattern = {"dart", "lua"},
callback = function()
if vim.bo.filetype == "dart" then
vim.bo.commentstring = "// %s"
elseif vim.bo.filetype == "lua" then
vim.bo.commentstring = "-- %s"
end
end
})
-- Safety check: prevent commenting in non-modifiable buffers
vim.api.nvim_create_autocmd("BufEnter", {
callback = function()
if not vim.bo.modifiable then vim.bo.commentstring = "" end
end
})
-- Wrap built-in commenting to prevent errors in non-modifiable buffers
vim.api.nvim_create_autocmd("BufWinEnter", {
callback = function()
local bufnr = vim.api.nvim_get_current_buf()
if not vim.api.nvim_buf_get_option(bufnr, "modifiable") then
-- Disable commentstring for non-modifiable buffers
vim.api.nvim_buf_set_option(bufnr, "commentstring", "")
end
end
})
-- Completion Setup
-- ================
local cmp = require("cmp")
cmp.setup({
sources = {{name = "nvim_lsp"}, {name = "buffer"}, {name = "path"}},
mapping = cmp.mapping.preset.insert({
["<C-Space>"] = cmp.mapping.complete(),
["<CR>"] = cmp.mapping.confirm({select = true}),
["<C-e>"] = cmp.mapping.abort()
})
})
-- DAP Visual Signs Configuration
-- ==============================
vim.fn.sign_define("DapBreakpoint", {
text = "●",
texthl = "DapBreakpoint",
linehl = "",
numhl = ""
})
vim.fn.sign_define("DapBreakpointCondition", {
text = "◆",
texthl = "DapBreakpointCondition",
linehl = "",
numhl = ""
})
vim.fn.sign_define("DapBreakpointRejected", {
text = "○",
texthl = "DapBreakpointRejected",
linehl = "",
numhl = ""
})
vim.fn.sign_define("DapStopped", {
text = "→",
texthl = "DapStopped",
linehl = "DapStoppedLine",
numhl = "DapStoppedLine"
})
vim.fn.sign_define("DapLogPoint", {
text = "◆",
texthl = "DapLogPoint",
linehl = "",
numhl = ""
})
-- General Keymaps
-- ===============
vim.keymap.set("n", "<leader>e", ":NvimTreeToggle<CR>",
{desc = "Toggle Explorer"})
vim.keymap
.set("n", "<leader>o", ":NvimTreeFocus<CR>", {desc = "Focus Explorer"})
-- LSP Keymaps (Lspsaga)
-- =====================
vim.keymap.set("n", "K", "<cmd>Lspsaga hover_doc<CR>", {desc = "Hover doc"})
vim.keymap.set("n", "gd", "<cmd>Lspsaga goto_definition<CR>",
{desc = "Goto definition"})
vim.keymap.set("n", "gp", "<cmd>Lspsaga peek_definition<CR>",
{desc = "Peek definition"})
-- Close Lspsaga windows easily
vim.api.nvim_create_autocmd("FileType", {
pattern = {"saga_hover", "saga_peek", "saga_finder", "sagaoutline"},
callback = function()
vim.keymap.set("n", "q", "<cmd>close<CR>",
{buffer = true, silent = true})
vim.keymap.set("n", "<Esc>", "<cmd>close<CR>",
{buffer = true, silent = true})
end
})
4. Add a Terminal Alias
echo "alias nvimf='NVIM_APPNAME=nvim-flutter nvim'" >> ~/.zshrc
source ~/.zshrc
Now run:
nvimf
On the first launch, plugins install themselves automatically.
What’s Inside This Configuration
Your final config is pretty stacked. Here's a human-readable rundown.
1. LSP + Treesitter + Cmp
You get:
- Dart LSP with Flutter-specific capabilities
- Treesitter highlighting and indentation
- Autocompletion (paths, buffer, LSP)
- Proper snippet-less workflow (you're welcome)
2. Full Debugging with DAP
Your setup includes:
- nvim-dap
- nvim-dap-ui
- nvim-dap-virtual-text
- DAP visual signs
- Auto-opening/closing UI panels
Flutter debugging works natively via:
<leader>db Start debugging
<leader>dc Continue
<leader>do Step over
<leader>di Step into
<leader>du Step out
<leader>dr Restart
<leader>ds Stop
<leader>dBp Toggle breakpoint
<leader>dBP Conditional breakpoint
<leader>dt Toggle DAP UI
3. Flutter Tools Integration
You wired up the official flutter-tools.nvim with:
- Structured UI
- Closing tags
- DAP support
- LSP config
- Web-server-optimized Flutter run command
Keybinds:
<leader>fr Run Flutter (web server 8008)
<leader>fq Quit Flutter
4. Comment.nvim with Safety Guards
You ditched the default gcc/gbc conflicts and built your own safe system:
gCc Comment line
gCb Comment block
gc Comment operator (motions & visual)
gB Block comment operator
If a buffer is unmodifiable, the config refuses to break itself.
Good.
5. UI Enhancements
Included:
- Catppuccin (Macchiato)
- Lualine
- Gitsigns
- Which-Key
- Rounded borders
- Cursorline
- Proper icon support
6. NvimTree Auto-open
File tree opens on launch.
<leader>e Toggle
<leader>o Focus
7. Lspsaga with Slimmed UI
Just the essentials:
- Hover
- Goto definition
- Peek definition
Also escape and q close saga windows because suffering is optional.
Final Keybind Reference
Flutter
| Key | Action |
|---|---|
<leader>fr |
Flutter Run (Web) |
<leader>fq |
Flutter Quit |
Debugging
| Key | Action |
|---|---|
<leader>db |
Start Debug |
<leader>dc |
Continue |
<leader>do |
Step Over |
<leader>di |
Step Into |
<leader>du |
Step Out |
<leader>dr |
Restart |
<leader>ds |
Stop |
<leader>dBp |
Toggle Breakpoint |
<leader>dBP |
Conditional Breakpoint |
<leader>dt |
Toggle DAP UI |
File Tree
| Key | Action |
|---|---|
<leader>e |
Toggle NvimTree |
<leader>o |
Focus on NvimTree |
Comments
| Key | Action |
|---|---|
gCc |
Line comment |
gCb |
Block comment |
gc |
Comment operator |
gB |
Block comment operator |
LSP
| Key | Action |
|---|---|
K |
Hover doc |
gd |
Goto definition |
gp |
Peek definition |
Final Thoughts
This configuration is basically a full IDE disguised as Neovim. It’s clean, isolated, stable, safer with commenting, and way more pleasant for Flutter than the official tooling. You can publish this as-is; nothing is missing.