FreeBSD15现代化Neovim生产力环境部署指南

在 FreeBSD 15 上从源码编译 Neovim 0.10+,并部署 lazy.nvim、nvim-tree、OSC 52 和 conform.nvim 组成的现代化编辑环境。

本文记录一套 FreeBSD 15 专用的 Neovim 生产力环境部署流程:先用 FreeBSD 的 pkg 准备编译环境,再从源码安装 Neovim 0.10+,最后写入一份基于 lazy.nvim 的 Lua 配置。

这套配置适合 FreeBSD 服务器、虚拟机和桌面环境使用,重点解决日常编辑里最常见的几件事:文件树、主题、远程 SSH 复制、保存时格式化、JSON/YAML/HTML/CSS/JavaScript/Markdown/Lua 格式化。

一、安装系统依赖并编译 Neovim

FreeBSD 使用 pkg 管理二进制包。编译 Neovim 时建议使用 gmake,不要直接用系统自带的 make

下面命令需要 root 权限。如果当前不是 root 用户,请在命令前加 doassudo

# 强制刷新仓库索引,并升级已安装软件包
pkg update -f
pkg upgrade -y

# 安装源码编译 Neovim 所需工具
pkg install -y ninja cmake gettext-tools gmake unzip curl git gcc

然后拉取 Neovim 源码并切换到稳定分支:

git clone https://github.com/neovim/neovim.git ~/neovim-src
cd ~/neovim-src
git checkout stable

开始编译并安装:

gmake CMAKE_BUILD_TYPE=Release -j$(sysctl -n hw.ncpu)
gmake install

安装后刷新 shell 命令缓存并验证版本:

hash -r
which nvim
nvim -v

如果之前通过 pkg 安装过旧版 Neovim,重点看 which nvim 的输出,确认当前优先调用的是刚编译安装的新版本。

二、安装格式化工具

这份配置使用 conform.nvim 统一调度外部格式化工具。FreeBSD 上可以直接安装 jqstylua,Node.js 与 npm 使用带版本号的包名。

pkg install -y node20 npm-node20 jq stylua
npm install -g prettier

安装完成后统一验证:

node -v
npm -v
jq --version
stylua --version
prettier -v

其中:

  • jq 用于格式化 JSON。
  • stylua 用于格式化 Lua。
  • prettier 用于格式化 YAML、HTML、CSS、JavaScript、TypeScript 和 Markdown。

三、写入统一 Lua 配置

Neovim 在 FreeBSD 下仍然使用 ~/.config/nvim/init.lua 作为用户配置文件。

远程 SSH 终端里不要直接粘贴超长 cat << EOF 脚本,容易出现粘贴错乱。建议先运行短命令清理旧配置并打开文件:

# 清理旧环境
rm -rf ~/.config/nvim ~/.local/share/nvim ~/.local/state/nvim ~/.cache/nvim

# 创建配置目录并打开 init.lua
mkdir -p ~/.config/nvim
nvim ~/.config/nvim/init.lua

进入空文件后按 i 进入插入模式,复制下面这段纯 Lua 配置粘贴进去,然后按 Esc,输入 :wq 保存退出。

-- Basic settings
vim.g.mapleader = ";"
local opt = vim.opt
opt.number = true
opt.relativenumber = true
opt.cursorline = true
opt.termguicolors = true
opt.encoding = "utf-8"
opt.mouse = "a"
opt.timeoutlen = 800
opt.splitright = true
opt.splitbelow = true

-- Install lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or 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)

-- Plugins
require("lazy").setup({
  spec = {
    -- 1. Theme
    {
      "navarasu/onedark.nvim",
      lazy = false,
      priority = 1000,
      config = function()
        require("onedark").setup({ style = "dark" })
        require("onedark").load()

        local hl = vim.api.nvim_set_hl
        hl(0, "Normal", { bg = "#1e2127", fg = "#abb2bf" })
        hl(0, "NonText", { bg = "#1e2127", fg = "#3b4048" })
        hl(0, "CursorLine", { bg = "#2c313a" })
        hl(0, "Visual", { bg = "#3e4452" })
        hl(0, "Search", { bg = "#61afef", fg = "#1e2127" })
        hl(0, "IncSearch", { bg = "#98c379", fg = "#1e2127" })
      end,
    },

    -- 2. File tree
    {
      "nvim-tree/nvim-tree.lua",
      dependencies = { "nvim-tree/nvim-web-devicons" },
      config = function()
        require("nvim-tree").setup({
          view = { width = 30, side = "left" },
          renderer = { indent_markers = { enable = true } },
        })
      end,
    },

    -- 3. Remote clipboard over SSH
    {
      "ojroques/nvim-osc52",
      config = function()
        require("osc52").setup()
        vim.api.nvim_create_autocmd("TextYankPost", {
          callback = function()
            if vim.v.event.operator == "y" and vim.v.event.regname == "" then
              require("osc52").copy_register("")
            end
          end,
        })
      end,
    },

    -- 4. Formatting
    {
      "stevearc/conform.nvim",
      lazy = false,
      config = function()
        require("conform").setup({
          formatters_by_ft = {
            lua = { "stylua" },
            json = { "jq" },
            yaml = { "prettier" },
            html = { "prettier" },
            css = { "prettier" },
            javascript = { "prettier" },
            typescript = { "prettier" },
            markdown = { "prettier" },
          },
          formatters = {
            jq = {
              command = "jq",
              args = { "--indent", "2", "." },
              stdin = true,
            },
          },
          format_on_save = {
            timeout_ms = 3000,
            lsp_format = "fallback",
          },
          notify_on_error = true,
        })
      end,
    },
  },
})

-- Keymaps
local map = vim.keymap.set

map("n", "<leader>w", ":w<CR>", { silent = true })
map("n", "<leader>q", ":q<CR>", { silent = true })
map("n", "<CR>", ":noh<CR><CR>", { silent = true })

-- Toggle or focus file tree: ;t
map("n", "<leader>t", function()
  local view = require("nvim-tree.view")
  if view.is_visible() then
    if vim.api.nvim_get_current_win() == view.get_winnr() then
      vim.cmd("wincmd p")
    else
      vim.cmd("NvimTreeFocus")
    end
  else
    vim.cmd("NvimTreeToggle")
  end
end, { silent = true })

-- Format manually: ;f
map({ "n", "v" }, "<leader>f", function()
  require("conform").format({
    async = false,
    timeout_ms = 3000,
    lsp_format = "fallback",
  }, function(err)
    if err then
      vim.notify("Format failed: " .. err, vim.log.levels.ERROR)
    else
      vim.notify("Format done", vim.log.levels.INFO)
    end
  end)
end, { silent = true })

-- Direct format shortcut: F3
map({ "n", "v" }, "<F3>", function()
  require("conform").format({
    async = false,
    timeout_ms = 3000,
    lsp_format = "fallback",
  }, function(err)
    if err then
      vim.notify("Format failed: " .. err, vim.log.levels.ERROR)
    else
      vim.notify("Format done", vim.log.levels.INFO)
    end
  end)
end, { silent = true })

-- Window navigation
map("n", "<C-h>", "<C-w>h")
map("n", "<C-l>", "<C-w>l")
map("n", "<C-j>", "<C-w>j")
map("n", "<C-k>", "<C-w>k")

四、首次启动 Neovim

执行:

nvim

首次启动时,lazy.nvim 会自动拉取并安装插件。配置正常时,插件管理界面里应该能看到这些插件处于可用状态:

  • conform.nvim
  • nvim-osc52
  • nvim-tree.lua
  • nvim-web-devicons
  • onedark.nvim

如果 lazy.nvim 提示有更新,可以在插件管理界面按 U 更新。确认插件安装完成后,按 q 退出管理界面,开始正常使用 Neovim。

如果 GitHub 访问不稳定,插件下载可能失败。确认网络后重新打开 Neovim,或者在 Neovim 内执行:

:Lazy sync

五、验证格式化功能

先测试 JSON 格式化:

nvim test.json

写入一行未格式化内容:

{"name":"freebsd","version":"15","editor":"neovim"}

回到普通模式后输入:

;f

如果配置正常,文件会被 jq 展开成多行:

{
  "name": "freebsd",
  "version": "15",
  "editor": "neovim"
}

也可以输入:

:w

保存时 conform.nvim 会自动执行格式化。如果 JSON 本身有语法错误,例如多了逗号或少了引号,jq 会拒绝格式化,这也正好能顺手检查配置文件是否写坏。

查看当前 buffer 可用 formatter:

:ConformInfo

注意::ConformInfo 只负责查看当前 buffer 的 formatter 状态,不会执行格式化。看到类似下面的状态,说明 jq 已经可用:

jq ready (json) /usr/local/bin/jq

真正执行格式化需要在普通模式下按 ;f、按 F3,或者保存文件:

:w

如果想绕过快捷键直接测试 jq,也可以执行:

:lua require("conform").format({ formatters = { "jq" }, async = false, timeout_ms = 3000 })

如果 JSON 格式化没有反应,先检查 jq 是否可用:

which jq
jq --version

如果 Lua 格式化没有反应,检查 stylua

which stylua
stylua --version

如果 YAML、HTML、CSS、JavaScript、TypeScript 或 Markdown 格式化没有反应,再检查 prettier

which prettier
prettier -v

六、常用快捷键

  • ;t:文件树与代码窗口智能切换。
  • ;f:格式化当前文件或选中范围。
  • F3:格式化当前文件或选中范围。
  • ;w:保存当前文件,并触发保存时格式化。
  • ;q:退出当前窗口。
  • Ctrl+h/j/k/l:在窗口之间移动。
  • Enter:清除搜索高亮。

七、FreeBSD 15 使用注意事项

FreeBSD 的第三方软件一般安装在 /usr/local 下,所以 jqstyluaprettiernvim 等命令通常会出现在 /usr/local/bin。如果 Neovim 找不到 formatter,先检查当前 shell 的 PATH 是否包含 /usr/local/bin

如果编译或运行时遇到 ld-elf.so.1 相关动态库错误,优先执行 pkg update -fpkg upgrade -y,确保基础系统、ports 包和依赖库版本一致。

如果是在远程 SSH 里使用复制功能,本地终端需要支持 OSC 52。iTerm2、WezTerm、Kitty、Windows Terminal 等现代终端通常支持,部分终端需要手动开启剪贴板访问。

图标能否正常显示,取决于本地终端字体是否安装 Nerd Fonts,例如 JetBrainsMono Nerd Font。如果没有 Nerd Font,nvim-web-devicons 仍能运行,只是图标可能显示成方块或乱码。

至此,FreeBSD 15 下的 Neovim 现代化编辑环境就部署完成了。后续只需要维护 ~/.config/nvim/init.lua,整套插件和格式化体验就能保持一致。