TL;DR
- The macOS IME problem can be solved with
im-select.nvim+macism. - On Neovim 0.11, watch out for plugin compatibility (use the
0.1.xtag for Telescope). - You can build a perfectly usable environment with a simple config that doesn’t need Nerd Font.
- For LSP and completion, the
mason.nvim+nvim-cmpcombo is the most beginner-friendly option.
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:
- Solving the macOS IME problem — the most common pain point for Japanese input
- Neovim 0.11 plugin compatibility — what to watch out for on the latest version
- A minimal config that doesn’t need Nerd Font — an environment that works without font setup
- Basic LSP / completion setup — go-to-definition and autocompletion
Prerequisites
The configuration in this article was verified on the following environment:
| Item | Version |
|---|---|
| macOS | Sonoma 14.x or later |
| Neovim | 0.11.3 or later (required for the new LSP API) |
| Homebrew | Installed |
| Terminal | iTerm2 (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:
-
im-select.nvim (Neovim plugin)
- Detects mode switches (Insert → Normal, etc.)
- Calls an external CLI tool to switch the IME
-
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:
| Setting | Pros | Cons |
|---|---|---|
{ "InsertEnter" } (default) | Convenient for typing Japanese in succession | Requires switching every time you write English code |
{} (disabled) | Always starts in ASCII, so behavior is predictable | Manual 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.
| Specification | Characteristics | Recommendation |
|---|---|---|
tag = "0.1.x" | Stable, Neovim 0.11–compatible | Recommended |
tag = "0.1.8" etc. | Pinned to a specific version | If reproducibility matters |
branch = "master" | Latest dev branch | If 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:
- Check GitHub Issues: Search for
[plugin name] Neovim 0.11. - Check the Requirements section in the README: The supported Neovim version is usually documented.
- Update to the latest version: Run
:Lazy syncto 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:
- Download Nerd Font.
- Install it on your system.
- Change the font setting in your terminal.
- Verify the changes took effect.
Why I went without Nerd Font
Reasons for skipping Nerd Font:
Pros:
- No need to fiddle with terminal font settings.
- Easier to share configs across multiple machines (no font install required).
- Simple, lightweight look.
Cons:
- Harder to tell file types at a glance.
- Visually plain.
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:
- Go to definition: Jump to where a function or variable is defined.
- Find references: List everywhere a function or variable is used.
- Autocompletion: Show candidates while typing.
- Diagnostics: Surface errors and warnings in real time.
- Rename: Rename a symbol everywhere at once.
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
| Concern | Choice | Reason |
|---|---|---|
| LSP installation | mason.nvim | GUI-managed, easy for beginners |
| Completion engine | nvim-cmp | Most widely used, lots of resources |
| Snippets | LuaSnip | Smooth 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 traditionallspconfig.xxx.setup({})form still works. For new setups, the new API is recommended since it reduces plugin dependencies.
Benefits of the new API:
- Built into Neovim, so it should remain stable long-term.
- Simple, easy-to-read syntax.
- Supports file-based configuration (under
~/.config/nvim/lsp/).
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:
| Approach | Description | Issue |
|---|---|---|
| Global keymaps | Active in every buffer | Keys are bound even in files without LSP |
| lspconfig.on_attach | Configured via lspconfig | Depends on lspconfig |
| LspAttach event | Set when LSP attaches | None |
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:
| Layout | Pros | Cons |
|---|---|---|
| Single file | Easy to see and search the whole thing | Hard to read once it gets long |
| Multiple files | Clear separation of concerns, scales better | Inter-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.
- IME problem: Solved with
im-select.nvim+macism. Settingset_previous_events = {}(always return to ASCII) is the most programming-friendly choice. - Neovim 0.11 compatibility: Use the
0.1.xtag for Telescope (already fixed). - No Nerd Font needed: Disable icons in each plugin to get a simple environment.
- 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
- im-select.nvim - Automatic IME switching
- macism - macOS IME control CLI
- nvim-lspconfig - LSP configuration
- mason.nvim - Language server management
- nvim-cmp - Completion engine
- telescope.nvim - Fuzzy finder
- lazy.nvim - Plugin manager