Skip to content

Neovim Configuration

Wojciech Kulik edited this page Oct 21, 2024 · 3 revisions

Below you will find basic information on how to prepare your Neovim for iOS development.

If you'd like to get more details, you can read my article: The complete guide to iOS & macOS development in Neovim.

 

🛋️  Sample Config for iOS

Here you can find my sample config for iOS development. You can try it out without touching your own config.

ios-dev-starter-nvim

 

🧰  LSP

Apple provides together with Xcode an LSP server called sourcekit-lsp. You can integrate it by using nvim-lspconfig plugin. To properly display code completion you will also need nvim-cmp. On top of that, you also need Build Server Protocol (BSP), which will let the LSP understand the project structure (xcodeproj / xcworkspace). For that purpose, you need to install xcode-build-server.

👉 nvim-lspconfig configuration
return {
  "neovim/nvim-lspconfig",
  event = { "BufReadPre", "BufNewFile" },
  dependencies = {
    "hrsh7th/cmp-nvim-lsp",
    { "antosha417/nvim-lsp-file-operations", config = true },
  },
  config = function()
    local lspconfig = require("lspconfig")
    local cmp_nvim_lsp = require("cmp_nvim_lsp")
    local keymap = vim.keymap -- for conciseness
    local opts = { noremap = true, silent = true }
    local on_attach = function(_, bufnr)
      opts.buffer = bufnr

      -- set keybindings
      opts.desc = "Show LSP definitions"
      keymap.set("n", "gd", "<cmd>Telescope lsp_definitions trim_text=true<cr>", opts)

      opts.desc = "Show LSP definitions in split"
      keymap.set("n", "gD", "<cmd>vsplit | Telescope lsp_definitions trim_text=true<cr>", opts)

      opts.desc = "Show LSP references"
      vim.keymap.set(
        "n",
        "gr",
        "<cmd>Telescope lsp_references trim_text=true include_declaration=false<cr>",
        opts
      )

      opts.desc = "Show LSP implementation"
      vim.keymap.set("n", "gi", "<cmd>Telescope lsp_implementations<cr>", opts)

      opts.desc = "Show LSP code actions"
      keymap.set({ "n", "v" }, "<leader>ca", vim.lsp.buf.code_action, opts)

      opts.desc = "Smart rename"
      keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)

      opts.desc = "Show buffer diagnostics"
      keymap.set("n", "<leader><leader>d", "<cmd>Telescope diagnostics bufnr=0<CR>", opts)

      opts.desc = "Go to previous diagnostic"
      keymap.set("n", "[d", function()
        vim.diagnostic.jump({ count = -1 })
        vim.cmd("normal! zz")
      end, opts)

      opts.desc = "Go to next diagnostic"
      keymap.set("n", "]d", function()
        vim.diagnostic.jump({ count = 1 })
        vim.cmd("normal! zz")
      end, opts)

      opts.desc = "Show documentation for what is under cursor"
      keymap.set("n", "K", vim.lsp.buf.hover, opts)

      opts.desc = "Restart LSP"
      keymap.set("n", "<leader>rl", ":LspRestart | LspStart<CR>", opts)
    end

    local capabilities = cmp_nvim_lsp.default_capabilities()

    local defaultLSPs = {
      "sourcekit",
    }

    for _, lsp in ipairs(defaultLSPs) do
      lspconfig[lsp].setup({
        capabilities = capabilities,
        on_attach = on_attach,
        cmd = lsp == "sourcekit" and { vim.trim(vim.fn.system("xcrun -f sourcekit-lsp")) } or nil,
      })
    end

    opts.desc = "Show line diagnostics"
    keymap.set("n", "<leader>d", vim.diagnostic.open_float, opts)
  end,
}
👉 nvim-cmp configuration
return {
  "hrsh7th/nvim-cmp",
  event = "InsertEnter",
  dependencies = {
    "hrsh7th/cmp-buffer", -- source for text in buffer
    "hrsh7th/cmp-path", -- source for file system paths
    "L3MON4D3/LuaSnip", -- snippet engine
    "saadparwaiz1/cmp_luasnip", -- for autocompletion
    "rafamadriz/friendly-snippets", -- useful snippets
    "onsails/lspkind.nvim", -- vs-code like pictograms
  },
  config = function()
    local cmp = require("cmp")
    local luasnip = require("luasnip")
    local lspkind = require("lspkind")

    -- loads vscode style snippets from installed plugins (e.g. friendly-snippets)
    require("luasnip.loaders.from_vscode").lazy_load()

    cmp.setup({
      completion = {
        completeopt = "menu,menuone,preview",
      },
      snippet = { -- configure how nvim-cmp interacts with snippet engine
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },
      mapping = cmp.mapping.preset.insert({
        ["<C-k>"] = cmp.mapping.select_prev_item(), -- previous suggestion
        ["<C-j>"] = cmp.mapping.select_next_item(), -- next suggestion
        ["<C-Space>"] = cmp.mapping.complete(), -- show completion suggestions
        ["<C-e>"] = cmp.mapping.abort(), -- close completion window
        ["<CR>"] = cmp.mapping.confirm({ select = false, behavior = cmp.ConfirmBehavior.Replace }),
        ["<C-b>"] = cmp.mapping(function(fallback)
          if luasnip.jumpable(-1) then
            luasnip.jump(-1)
          else
            fallback()
          end
        end, { "i", "s" }),
        ["<C-f>"] = cmp.mapping(function(fallback)
          if luasnip.jumpable(1) then
            luasnip.jump(1)
          else
            fallback()
          end
        end, { "i", "s" }),
      }),
      -- sources for autocompletion
      sources = cmp.config.sources({
        { name = "nvim_lsp" },
        { name = "luasnip" }, -- snippets
        { name = "buffer" }, -- text within current buffer
        { name = "path" }, -- file system paths
      }),
      -- configure lspkind for vs-code like pictograms in completion menu
      formatting = {
        format = lspkind.cmp_format({
          maxwidth = 50,
          ellipsis_char = "...",
        }),
      },
    })
  end,
}
👉 xcode-build-server configuration

First, you need to install it:

brew install xcode-build-server

Then, you can configure it for your project:

xcode-build-server config -workspace <xcworkspace> -scheme <scheme> 
# or
xcode-build-server config -project <xcodeproj> -scheme <scheme> 

Make sure to call it from your project root directory. It should create buildServer.json. Open it and make sure that all the information there is correct.

Once all steps are finished, you should be able to open your project in Neovim and see working code completion. If something doesn't work, make sure to run a clean build from Xcode first. Also, you can run :LspInfo to see if the LSP is properly attached and the root directory is detected.

 

👨‍🎓  SwiftFormat

SwiftFormat is a very popular tool to keep formatting consistent across the project. You can easily integrate it with Neovim by using conform.nvim plugin.

Here is a sample config:

return {
  "stevearc/conform.nvim",
  event = { "BufReadPre", "BufNewFile" },
  config = function()
    local conform = require("conform")

    conform.setup({
      formatters_by_ft = {
        swift = { "swiftformat" },
      },
      format_on_save = function(bufnr)
        local ignore_filetypes = { "oil" }
        if vim.tbl_contains(ignore_filetypes, vim.bo[bufnr].filetype) then
          return
        end

        return { timeout_ms = 500, lsp_fallback = true }
      end,
      log_level = vim.log.levels.ERROR,
    })
  end,
}

The code will be automatically formatted on save event 🔥.

 

👮‍♂️  SwiftLint

Usually, you also need some linter to detect common issues. You can easily integrate SwiftLint using nvim-lint plugin.

Here is a sample config:

return {
  "mfussenegger/nvim-lint",
  event = { "BufReadPre", "BufNewFile" },
  config = function()
    local lint = require("lint")

    lint.linters_by_ft = {
      swift = { "swiftlint" },
    }

    local lint_augroup = vim.api.nvim_create_augroup("lint", { clear = true })

    vim.api.nvim_create_autocmd({ "BufWritePost", "BufReadPost", "InsertLeave", "TextChanged" }, {
      group = lint_augroup,
      callback = function()
        if not vim.endswith(vim.fn.bufname(), "swiftinterface") then
          require("lint").try_lint()
        end
      end,
    })

    vim.keymap.set("n", "<leader>ml", function()
      require("lint").try_lint()
    end, { desc = "Lint file" })
  end,
}
Clone this wiki locally