Enhanced Neovim Setup for Flutter Development on macOS

Enhanced Neovim Setup for Flutter Development on macOS

Minimal setup:

Minimal Neovim + Flutter Setup (macOS)
Isolated. Beautiful. Fast. No Vim conflict.

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

~/.config/nvim-flutter/init.lua
~/.config/nvim-flutter/init.lua. GitHub Gist: instantly share code, notes, and snippets.

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.