Skip to main content

A Practical Neovim Setup Guide for macOS

Cover image for A Practical Neovim Setup Guide for macOS

TL;DR

Background

This article walks through a Neovim setup that’s comfortable to use on macOS. It assumes you know the basic Vim operations (mode switching, cursor movement, save/quit, etc.) but are new to configuring Neovim plugins.

It covers four topics:

  1. Solving the macOS IME problem — the most common pain point for Japanese input
  2. Neovim 0.11 plugin compatibility — what to watch out for on the latest version
  3. A minimal config that doesn’t need Nerd Font — an environment that works without font setup
  4. Basic LSP / completion setup — go-to-definition and autocompletion

Prerequisites

The configuration in this article was verified on the following environment:

ItemVersion
macOSSonoma 14.x or later
Neovim0.11.3 or later (required for the new LSP API)
HomebrewInstalled
TerminaliTerm2 (Terminal.app also works)

If you haven’t installed Neovim yet:

brew install neovim

1. Solving the macOS IME Problem

The problem

When using Neovim (Vim) on macOS, many people get tripped up by the IME (Japanese input) issue.

Specifically: if you return to Normal mode while still in Japanese input mode, commands like j and k stop working.

The reason: even after switching from Insert back to Normal, macOS keeps the IME state as-is. After typing some Japanese and pressing Esc, the IME is still in Japanese mode — so typing jjj... produces っっっ... instead of moving the cursor.

The fix: im-select.nvim + macism

The fix combines two components:

  1. im-select.nvim (Neovim plugin)

    • Detects mode switches (Insert → Normal, etc.)
    • Calls an external CLI tool to switch the IME
  2. macism (CLI tool)

    • The command that actually flips the macOS IME
    • Invoked from im-select.nvim

So im-select.nvim watches for mode changes inside Neovim, and when it sees one, it runs macism to switch the IME back to ASCII.

Why macism

im-select.nvim shells out to a CLI tool to switch the IME on macOS. Several CLI tools exist for this, but the official im-select.nvim README recommends macism:

Please install macism, this is the only one CLI tool can switch CJK and English input methods in macOS correctly. — im-select.nvim README

macism reliably switches CJK input sources like Japanese. With other tools (input-source-switcher and similar), there’s a known macOS bug where the menu bar icon flips but the input source doesn’t actually change.

Installation

macism isn’t in the official Homebrew tap, so you need to add a third-party tap:

# Add the tap
brew tap laishulu/homebrew

# Install macism
brew install laishulu/homebrew/macism

Once installed, verify it works:

# Check the current input source
macism

# Example output: com.apple.inputmethod.Kotoeri.RomajiTyping.Japanese
# or:            com.apple.keylayout.ABC

Neovim configuration

Use the im-select.nvim plugin to control behavior on mode transitions.

-- Example config for the lazy.nvim plugin manager
{
  "keaising/im-select.nvim",
  config = function()
    require("im_select").setup({
      -- Target input source (ASCII keyboard)
      default_im_select = "com.apple.keylayout.ABC",

      -- Absolute path to macism (use /usr/local/bin/macism on Intel Mac)
      default_command = "/opt/homebrew/bin/macism",

      -- When to switch to ASCII
      set_default_events = {
        "VimEnter",       -- On Neovim startup
        "FocusGained",    -- When the window regains focus
        "InsertLeave",    -- When leaving Insert mode
        "CmdlineLeave"    -- When leaving command-line mode
      },

      -- Setting to restore the previous IME on InsertEnter (empty = disabled)
      set_previous_events = {},
    })
  end,
}

Why I set set_previous_events = {}

By default, set_previous_events = { "InsertEnter" }, which restores the previous IME state when entering Insert mode.

I disabled this by setting it to an empty table. Reasoning:

Trade-off comparison:

SettingProsCons
{ "InsertEnter" } (default)Convenient for typing Japanese in successionRequires switching every time you write English code
{} (disabled)Always starts in ASCII, so behavior is predictableManual switch needed when writing Japanese

For programming, English input dominates by far, so “always return to ASCII” feels much smoother. When I do need Japanese, I switch manually — no big deal.

If you write a lot of Japanese documentation, the default { "InsertEnter" } may be more convenient.


2. Neovim 0.11 Plugin Compatibility

Background

Neovim 0.11, released in March 2025, made significant changes to the LSP-related APIs. As a result, some plugins won’t work as-is.

Telescope.nvim compatibility issue

The most common one users hit is with Telescope.nvim (the fuzzy finder).

Symptom: Errors when opening Telescope, or broken display.

Cause: The Telescope.nvim stable branch (0.1.x) hadn’t caught up with the Neovim 0.11 API changes.

Fix: Use the 0.1.x tag (the latest version is already patched).

-- Neovim 0.11–compatible version
{
  "nvim-telescope/telescope.nvim",
  tag = "0.1.x",  -- Stable tag (Neovim 0.11–compatible)
  dependencies = { "nvim-lua/plenary.nvim" },
}

Choosing a version

As of late 2025, Telescope v0.2.0 has also been released.

SpecificationCharacteristicsRecommendation
tag = "0.1.x"Stable, Neovim 0.11–compatibleRecommended
tag = "0.1.8" etc.Pinned to a specific versionIf reproducibility matters
branch = "master"Latest dev branchIf you want to try new features

The 0.1.x branch was previously incompatible with Neovim 0.11, but that’s been fixed. Unless you have a specific reason otherwise, tag = "0.1.x" is the way to go.

Other ways to check compatibility

When a plugin doesn’t work, here’s how I investigate:

  1. Check GitHub Issues: Search for [plugin name] Neovim 0.11.
  2. Check the Requirements section in the README: The supported Neovim version is usually documented.
  3. Update to the latest version: Run :Lazy sync to refresh plugins.

3. A Minimal Config That Doesn’t Need Nerd Font

What is Nerd Font

Most Neovim setup articles tell you to “install Nerd Font.” Nerd Font is a font family that adds icons (file types, Git status, folders, and so on) on top of regular fonts.

But getting Nerd Font running requires several steps:

  1. Download Nerd Font.
  2. Install it on your system.
  3. Change the font setting in your terminal.
  4. Verify the changes took effect.

Why I went without Nerd Font

Reasons for skipping Nerd Font:

Pros:

Cons:

In actual coding, you can already tell the file type from the filename, so the lack of icons isn’t a practical problem.

Disabling icons in plugins

How to disable icons in the major plugins.

nvim-tree (file explorer)

{
  "nvim-tree/nvim-tree.lua",
  config = function()
    require("nvim-tree").setup({
      renderer = {
        icons = {
          show = {
            file = false,        -- Hide file icons
            folder = false,      -- Hide folder icons
            folder_arrow = true, -- Show expand/collapse arrows
            git = false,         -- Hide Git status icons
          },
          glyphs = {
            folder = {
              arrow_closed = ">",  -- Arrow when collapsed
              arrow_open = "v",    -- Arrow when expanded
            },
          },
        },
      },
    })
  end,
}

lualine (status line)

{
  "nvim-lualine/lualine.nvim",
  config = function()
    require("lualine").setup({
      options = {
        icons_enabled = false,       -- Disable all icons
        section_separators = "",     -- No section separators
        component_separators = "|",  -- Simple component separator
      },
    })
  end,
}

Visual comparison

With Nerd Font:

  init.lua    main   lua  utf-8  100%  42:1

Without Nerd Font:

NORMAL | main | init.lua | lua | utf-8 | 100% | 42:1

Information shows up as text instead of icons. Once you get used to it, it’s perfectly readable.


4. Basic LSP / Completion Setup

What LSP is

LSP (Language Server Protocol) is a communication protocol between editors and language servers. It enables features like:

Plugin layout overview

To get LSP and completion working, combine these plugins:

mason.nvim              # Language server installer
  └── mason-lspconfig   # Bridge between mason and lspconfig
        └── nvim-lspconfig  # Language server configuration

nvim-cmp                # Completion engine
  ├── cmp-nvim-lsp      # Completion source from LSP
  ├── cmp-buffer        # Complete words from the current buffer
  ├── cmp-path          # Complete file paths
  └── LuaSnip           # Snippet expansion

Why this stack

ConcernChoiceReason
LSP installationmason.nvimGUI-managed, easy for beginners
Completion enginenvim-cmpMost widely used, lots of resources
SnippetsLuaSnipSmooth integration with nvim-cmp

Auto-installing language servers with mason.nvim

With mason.nvim, you can manage language servers from a GUI via the :Mason command. With mason-lspconfig, you can also auto-install the language servers you need.

{
  "neovim/nvim-lspconfig",
  dependencies = {
    "williamboman/mason.nvim",
    "williamboman/mason-lspconfig.nvim",
  },
  config = function()
    -- Initialize mason (enables the :Mason command)
    require("mason").setup()

    -- Specify which language servers to auto-install
    require("mason-lspconfig").setup({
      ensure_installed = {
        "lua_ls",   -- Lua
        "ts_ls",    -- TypeScript/JavaScript
        "pyright",  -- Python
      },
    })
  end,
}

On first launch, the specified language servers are installed automatically.

Neovim 0.11’s new LSP configuration API

Neovim 0.11 changed how LSP is configured significantly.

The old way (depends on nvim-lspconfig):

local lspconfig = require("lspconfig")
lspconfig.lua_ls.setup({
  settings = {
    Lua = {
      diagnostics = { globals = { "vim" } },
    },
  },
})

The new way (Neovim 0.11.3+ native):

-- Per-language-server configuration
vim.lsp.config('lua_ls', {
  settings = {
    Lua = {
      diagnostics = { globals = { "vim" } },
    },
  },
})

-- Enable language servers
vim.lsp.enable({ "lua_ls", "pyright", "ts_ls" })

Note: nvim-lspconfig isn’t deprecated. Internally it functions as a wrapper that calls vim.lsp.config, so the traditional lspconfig.xxx.setup({}) form still works. For new setups, the new API is recommended since it reduces plugin dependencies.

Benefits of the new API:

LSP keymap setup

Bind LSP features to keys. Using the LspAttach event ensures keymaps are only set on buffers where LSP is active.

vim.api.nvim_create_autocmd("LspAttach", {
  callback = function(args)
    local bufnr = args.buf
    local opts = { buffer = bufnr, silent = true }

    -- Jump to definition / declaration / implementation
    vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
    vim.keymap.set("n", "gD", vim.lsp.buf.declaration, opts)
    vim.keymap.set("n", "gi", vim.lsp.buf.implementation, opts)

    -- Documentation and edit operations
    vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
    vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)
    vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)

    -- Navigate diagnostics
    vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, opts)
    vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts)
    vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float, opts)
  end,
})

Why use the LspAttach event

There are several ways to set up keymaps:

ApproachDescriptionIssue
Global keymapsActive in every bufferKeys are bound even in files without LSP
lspconfig.on_attachConfigured via lspconfigDepends on lspconfig
LspAttach eventSet when LSP attachesNone

LspAttach is a built-in Neovim event, so it doesn’t depend on any plugin and works reliably.

Completion setup with nvim-cmp

Basic configuration for the nvim-cmp completion engine.

{
  "hrsh7th/nvim-cmp",
  dependencies = {
    "hrsh7th/cmp-nvim-lsp",   -- LSP completion source
    "hrsh7th/cmp-buffer",     -- Buffer completion
    "hrsh7th/cmp-path",       -- Path completion
    "L3MON4D3/LuaSnip",       -- Snippet engine
    "saadparwaiz1/cmp_luasnip", -- Snippet completion
  },
  config = function()
    local cmp = require("cmp")
    local luasnip = require("luasnip")

    cmp.setup({
      -- Snippet expansion settings
      snippet = {
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },

      -- Keymaps
      mapping = cmp.mapping.preset.insert({
        ["<C-Space>"] = cmp.mapping.complete(),   -- Manually trigger completion
        ["<C-e>"] = cmp.mapping.abort(),          -- Cancel completion
        ["<CR>"] = cmp.mapping.confirm({ select = true }), -- Confirm
        ["<Tab>"] = cmp.mapping(function(fallback)
          if cmp.visible() then
            cmp.select_next_item()  -- Next candidate
          else
            fallback()
          end
        end, { "i", "s" }),
        ["<S-Tab>"] = cmp.mapping(function(fallback)
          if cmp.visible() then
            cmp.select_prev_item()  -- Previous candidate
          else
            fallback()
          end
        end, { "i", "s" }),
      }),

      -- Completion source priority
      sources = cmp.config.sources({
        { name = "nvim_lsp" },  -- LSP (highest priority)
        { name = "luasnip" },   -- Snippets
      }, {
        { name = "buffer" },    -- Words in the buffer
        { name = "path" },      -- File paths
      }),
    })
  end,
}

The completion sources are prioritized: the first group (LSP, snippets) takes precedence, and if there are no matches, candidates come from the next group (buffer, path).


On Splitting the Config File

All the configuration in this article lives in a single ~/.config/nvim/init.lua.

Why a single file:

LayoutProsCons
Single fileEasy to see and search the whole thingHard to read once it gets long
Multiple filesClear separation of concerns, scales betterInter-file dependencies get complex

For a config of around 400 lines, a single file is plenty manageable. You can always split it up once it grows.


Summary

This article covered four configurations for using Neovim comfortably on macOS.

  1. IME problem: Solved with im-select.nvim + macism. Setting set_previous_events = {} (always return to ASCII) is the most programming-friendly choice.
  2. Neovim 0.11 compatibility: Use the 0.1.x tag for Telescope (already fixed).
  3. No Nerd Font needed: Disable icons in each plugin to get a simple environment.
  4. LSP / completion: The mason.nvim + nvim-cmp combo gives you GUI-managed installation and rich completion.

This setup is just one option among many. As you use it, customize it to match your taste and use cases.

References

Official documentation

Plugins

ZSL
ZSL

AI Engineer

Researching and practicing development workflows powered by Generative AI.