The whole information to iOS & macOS growth in Neovim
In my earlier submit, I simply scratched the floor of iOS growth in Neovim. Since then I found many new issues that allowed me to maneuver my growth virtually fully to Neovim.
On this article, I’ll describe step-by-step easy methods to configure Neovim to maneuver away from Xcode. It took me a number of months to determine all of it out piece by piece and to mix it into one working iOS growth setting (I did it so that you don’t must :D). Hopefully, it gained’t take you greater than half a day to configure it with my assist :).
It is going to be somewhat bit prolonged journey, and it’ll require organising a number of plugins. I might suggest it to people who find themselves already acquainted with Vim. If you happen to simply put in Neovim, it might be overwhelming to study Vim motions, Neovim setting, and arrange dependencies, unexpectedly.
If you’re simply beginning with Neovim, take it slowly. First, study Vim motions inside Xcode (by enabling Vim mode), within the meantime begin configuring Neovim and get acquainted with it by putting in plugins, enhancing textual content recordsdata, JSON recordsdata, and so on. As soon as you are feeling snug with Vim motions and Neovim, then attempt migrating your growth :).
Limitations
In fact, there are going to be some limitations, that we are able to’t work round with different instruments, however should you observe this information, you must be capable of do over 90% of the work in the one you love Neovim.
To start with, should you haven’t completed it but, you must neglect about xcodeproj
and xcworkspace
. One other genius Xcode’s invention, fully unmaintainable by something however Xcode.
Begin utilizing XcodeGen (really helpful) or Tuist as an alternative! These instruments allow you to generate a undertaking primarily based on a easy configuration file. It’s required to simply add new recordsdata, targets, and capabilities outdoors of Xcode. They supply full undertaking administration with out touching Xcode.
Options That Require Xcode
- Extra superior debugging (sanitizers, reminiscence graphs)
- UI debugging (view hierarchy)
- Monitoring of reminiscence consumption, CPU utilization, power effectivity
- SwiftUI previews (they cease working ultimately anyway)
- Check debugging (unsure but whether it is doable outdoors of Xcode)
- Code protection (most likely doable to attain in Neovim)
- Signing administration
- Archiving and releasing app (doable by way of command line, however not supported but)
- Debugging StoreKit 2
- Xcode Cloud administration and integration
- Debugging on bodily units
- UI automated exams
- In all probability sport and visionOS growth?
- Code recordsdata in languages aside from Swift
- Property administration –
xcassets
is only a set of easy JSON recordsdata and folders, however
it’s nonetheless most likely simpler to do it by means of the built-in editor in Xcode - You could use XcodeGen or Tuist. In any other case, you’ll need so as to add recordsdata and handle
the undertaking configuration from Xcode.
If these usually are not an enormous a part of your every day work, it is possible for you to to exchange Xcode with Neovim.
Code Completion
An important factor for app growth for my part is the code completion. It’s arduous to think about growth with out that.
Additionally, it was probably the most difficult factor to resolve in Neovim. Why? As a result of Apple is being Apple and shares both none or very restricted instruments that couldn’t be reused by different editors.
Language Server Protocol
These days, we’ve got numerous programming languages. It wouldn’t be doable to keep up all code completions if each language would require a customized integration. That’s why Microsoft launched a unified means to try this and outlined Language Server Protocol (LSP). Kudos to Visible Studio Code! Now, every language is chargeable for offering code completion by implementing LSP.
The issue with Swift + iOS/macOS SDK is that the sourcekit-lsp supplied by Apple doesn’t perceive Xcode initiatives. So how may it present the code completion if it doesn’t perceive the undertaking structure, targets, dependencies, and so on.? Precisely, it might probably’t.
I spent a number of weeks on the lookout for some options, posting right here and there, and attempting many issues. I even acquired info from folks engaged on Swift that it gained’t be doable. Nonetheless, I didn’t surrender and at last, I posted on sourcekit-lsp GitHub a function request to supply assist for iOS growth.
Surprisingly, SolaWing appeared and posted there about his device that covers the hole between sourcekit-lsp and Xcode undertaking. He constructed a device known as xcode-build-server that implements Construct Server Protocol (BSP) to supply all the required details about the undertaking to sourcekit-lsp. And it really works like a appeal!
sourcekit-lsp and xcode-build-server
The device created by SolaWing is de facto nice. You solely must run one command in your undertaking listing and that’s it.
First, it’s important to obtain xcode-build-server. Simply clone the repository and place it in a folder the place you wish to maintain it.
Now, create a symbolic hyperlink to the binary from this folder to ensure that xcode-build-server
is globally seen:
sudo ln –s PATH/TO/xcode–construct–server /usr/native/bin |
As soon as it’s completed, just one extra step is required. It’s important to create a buildServer.json
that may inform LSP to speak with xcode-build-server
. To do you can merely run one command (almost certainly, solely as soon as to your undertaking lifetime):
# should you use workspace:
xcode-build-server config -scheme <XXX> -workspace *.xcworkspace
# or should you solely have a undertaking file:
xcode-build-server config -scheme <XXX> -project *.xcodeproj
# *.xcworkspace or *.xcodeproj must be distinctive. # It may be omitted and can auto select the distinctive workspace or undertaking.
# should you use workspace: xcode–construct–server config –scheme <XXX> –workspace *.xcworkspace # or should you solely have a undertaking file: xcode–construct–server config –scheme <XXX> –undertaking *.xcodeproj |
In case your undertaking is just not within the root listing, you must transfer the generated buildServer.json
to the foundation.
Neovim LSP Integration
An important step is completed. Now, our xcode-build-server
will be capable of present info
in regards to the undertaking. We simply must combine our Neovim with sourcekit-lsp
.
To try this we’ll want nvim-lspconfig plugin. Here’s a pattern configuration:
opts.desc = “Present line diagnostics”
vim.keymap.set(“n”, “<chief>d”, vim.diagnostic.open_float, opts)
opts.desc = “Present documentation for what’s beneath cursor”
vim.keymap.set(“n”, “Okay”, vim.lsp.buf.hover, opts)
finish
lspconfig[“sourcekit”].setup({
capabilities = capabilities,
on_attach = on_attach,
cmd = {
“/Purposes/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp”,
},
root_dir = operate(filename, _)
return util.root_pattern(“buildServer.json”)(filename)
or util.root_pattern(“*.xcodeproj”, “*.xcworkspace”)(filename)
or util.find_git_ancestor(filename)
or util.root_pattern(“Package deal.swift”)(filename)
finish,
})
finish,
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
return { “neovim/nvim-lspconfig”, occasion = { “BufReadPre”, “BufNewFile” }, dependencies = { “hrsh7th/cmp-nvim-lsp”, { “antosha417/nvim-lsp-file-operations”, config = true }, }, config = operate() native lspconfig = require(“lspconfig”) native util = require(“lspconfig.util”) native cmp_nvim_lsp = require(“cmp_nvim_lsp”) native capabilities = cmp_nvim_lsp.default_capabilities() native opts = { noremap = true, silent = true } native on_attach = operate(_, bufnr) opts.buffer = bufnr
opts.desc = “Present line diagnostics” vim.keymap.set(“n”, “<chief>d”, vim.diagnostic.open_float, opts)
opts.desc = “Present documentation for what’s beneath cursor” vim.keymap.set(“n”, “Okay”, vim.lsp.buf.hover, opts) finish
lspconfig[“sourcekit”].setup({ capabilities = capabilities, on_attach = on_attach, cmd = { “/Purposes/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp”, }, root_dir = operate(filename, _) return util.root_pattern(“buildServer.json”)(filename) or util.root_pattern(“*.xcodeproj”, “*.xcworkspace”)(filename) or util.find_git_ancestor(filename) or util.root_pattern(“Package deal.swift”)(filename) finish, }) finish, } |
So as to add assist for code completion pop-ups you should utilize nvim-cmp.
— masses vscode fashion snippets from put in 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
broaden = operate(args)
luasnip.lsp_expand(args.physique)
finish,
},
mapping = cmp.mapping.preset.insert({
[“<C-k>”] = cmp.mapping.select_prev_item(), — earlier suggestion
[“<C-j>”] = cmp.mapping.select_next_item(), — subsequent suggestion
[“<C-Space>”] = cmp.mapping.full(), — present completion strategies
[“<C-e>”] = cmp.mapping.abort(), — shut completion window
[“<CR>”] = cmp.mapping.verify({ choose = false, habits = cmp.ConfirmBehavior.Substitute }),
[“<C-b>”] = cmp.mapping(operate(fallback)
if luasnip.jumpable(-1) then
luasnip.soar(-1)
else
fallback()
finish
finish, { “i”, “s” }),
[“<C-f>”] = cmp.mapping(operate(fallback)
if luasnip.jumpable(1) then
luasnip.soar(1)
else
fallback()
finish
finish, { “i”, “s” }),
}),
— sources for autocompletion
sources = cmp.config.sources({
{ title = “nvim_lsp” },
{ title = “luasnip” }, — snippets
{ title = “buffer” }, — textual content inside present buffer
{ title = “path” }, — file system paths
}),
— configure lspkind for vs-code like pictograms in completion menu
formatting = {
format = lspkind.cmp_format({
maxwidth = 50,
ellipsis_char = “…”,
}),
},
})
finish,
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
return { “hrsh7th/nvim-cmp”, occasion = “InsertEnter”, dependencies = { “hrsh7th/cmp-buffer”, — supply for textual content in buffer “hrsh7th/cmp-path”, — supply for file system paths “L3MON4D3/LuaSnip”, — snippet engine “saadparwaiz1/cmp_luasnip”, — for autocompletion “rafamadriz/friendly-snippets”, — helpful snippets “onsails/lspkind.nvim”, — vs-code like pictograms }, config = operate() native cmp = require(“cmp”) native luasnip = require(“luasnip”) native lspkind = require(“lspkind”)
— masses vscode fashion snippets from put in 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 broaden = operate(args) luasnip.lsp_expand(args.physique) finish, }, mapping = cmp.mapping.preset.insert({ [“<C-k>”] = cmp.mapping.select_prev_item(), — earlier suggestion [“<C-j>”] = cmp.mapping.select_next_item(), — subsequent suggestion [“<C-Space>”] = cmp.mapping.full(), — present completion strategies [“<C-e>”] = cmp.mapping.abort(), — shut completion window [“<CR>”] = cmp.mapping.verify({ choose = false, habits = cmp.ConfirmBehavior.Substitute }), [“<C-b>”] = cmp.mapping(operate(fallback) if luasnip.jumpable(–1) then luasnip.soar(–1) else fallback() finish finish, { “i”, “s” }), [“<C-f>”] = cmp.mapping(operate(fallback) if luasnip.jumpable(1) then luasnip.soar(1) else fallback() finish finish, { “i”, “s” }), }), — sources for autocompletion sources = cmp.config.sources({ { title = “nvim_lsp” }, { title = “luasnip” }, — snippets { title = “buffer” }, — textual content inside present buffer { title = “path” }, — file system paths }), — configure lspkind for vs-code like pictograms in completion menu formatting = { format = lspkind.cmp_format({ maxwidth = 50, ellipsis_char = “…”, }), }, }) finish, } |
Abstract
Now the autocompletion ought to work nice inside any iOS and macOS undertaking. Simply be certain that to open Neovim within the root listing of your undertaking. Open any Swift file and run :LspInfo
command to see if the LSP server is correctly connected and if the foundation listing is ready.
If you happen to encounter any issues, normally constructing the undertaking from Xcode and operating xcode-build-server
command once more will resolve points. Typically buildServer.json
might be corrupted. Open it and ensure that the paths are appropriate.
Linting & Formatting
The following milestone in our growth in Neovim is organising a linter and formatter. The most well-liked selections are SwiftLint and SwiftFormat, so let’s go along with them. Each could be put in utilizing Homebrew.
SwiftLint
For linting we’ll use a plugin known as nvim-lint. Sadly, it doesn’t assist SwiftLint by default, however I used to be in a position to create a working configuration on my own.
— swiftlint
native sample = “[^:]+:(%d+):(%d+): (%w+): (.+)”
native teams = { “lnum”, “col”, “severity”, “message” }
native defaults = { [“source”] = “swiftlint” }
native severity_map = {
[“error”] = vim.diagnostic.severity.ERROR,
[“warning”] = vim.diagnostic.severity.WARN,
}
— discover .swiftlint.yml config file within the working listing
— might be simplified should you maintain it at all times within the root listing
native swiftlintConfigs =
vim.fn.systemlist({ “discover”, vim.fn.getcwd(), “-iname”, “.swiftlint.yml”, “-not”, “-path”, “*/.*/*” })
desk.kind(swiftlintConfigs, operate(a, b)
return a ~= “” and #a < #b
finish)
native selectedSwiftlintConfig
if swiftlintConfigs[1] then
selectedSwiftlintConfig = string.match(swiftlintConfigs[1], “^%s*(.-)%s*$”)
finish
lint.linters.swiftlint = {
cmd = “swiftlint”,
stdin = false,
args = {
“lint”,
“–force-exclude”,
“–use-alternative-excluding”,
“–config”,
selectedSwiftlintConfig or os.getenv(“HOME”) .. “/.config/nvim/.swiftlint.yml”, — change path if wanted
},
stream = “stdout”,
ignore_exitcode = true,
parser = require(“lint.parser”).from_pattern(sample, teams, severity_map, defaults),
}
— setup
lint.linters_by_ft = {
swift = { “swiftlint” },
}
native lint_augroup = vim.api.nvim_create_augroup(“lint”, { clear = true })
vim.api.nvim_create_autocmd({ “BufWritePost”, “BufReadPost” }, {
group = lint_augroup,
callback = operate()
require(“lint”).try_lint()
finish,
})
vim.keymap.set(“n”, “<chief>ml”, operate()
require(“lint”).try_lint()
finish, { desc = “Lint file” })
finish,
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
return { “mfussenegger/nvim-lint”, occasion = { “BufReadPre”, “BufNewFile” }, config = operate() native lint = require(“lint”)
— swiftlint native sample = “[^:]+:(%d+):(%d+): (%w+): (.+)” native teams = { “lnum”, “col”, “severity”, “message” } native defaults = { [“source”] = “swiftlint” } native severity_map = { [“error”] = vim.diagnostic.severity.ERROR, [“warning”] = vim.diagnostic.severity.WARN, }
— discover .swiftlint.yml config file within the working listing — might be simplified should you maintain it at all times within the root listing native swiftlintConfigs = vim.fn.systemlist({ “discover”, vim.fn.getcwd(), “-iname”, “.swiftlint.yml”, “-not”, “-path”, “*/.*/*” })
desk.kind(swiftlintConfigs, operate(a, b) return a ~= “” and #a < #b finish)
native selectedSwiftlintConfig if swiftlintConfigs[1] then selectedSwiftlintConfig = string.match(swiftlintConfigs[1], “^%s*(.-)%s*$”) finish
lint.linters.swiftlint = { cmd = “swiftlint”, stdin = false, args = { “lint”, “–force-exclude”, “–use-alternative-excluding”, “–config”, selectedSwiftlintConfig or os.getenv(“HOME”) .. “/.config/nvim/.swiftlint.yml”, — change path if wanted }, stream = “stdout”, ignore_exitcode = true, parser = require(“lint.parser”).from_pattern(sample, teams, severity_map, defaults), }
— setup lint.linters_by_ft = { swift = { “swiftlint” }, }
native lint_augroup = vim.api.nvim_create_augroup(“lint”, { clear = true })
vim.api.nvim_create_autocmd({ “BufWritePost”, “BufReadPost” }, { group = lint_augroup, callback = operate() require(“lint”).try_lint() finish, })
vim.keymap.set(“n”, “<chief>ml”, operate() require(“lint”).try_lint() finish, { desc = “Lint file” }) finish, } |
The strategy above has the benefit of with the ability to exclude recordsdata outlined in your .swiftlint.yml
config, as a result of the file title is handed to the device. Nonetheless, this manner linting outcomes can solely be up to date should you save your adjustments.
If you wish to have reside updates with out saving, however with out excluding recordsdata, you could possibly do:
vim.api.nvim_create_autocmd({ “BufWritePost”, “BufReadPost”, “InsertLeave”, “TextChanged” }, {
group = lint_augroup,
callback = operate()
require(“lint”).try_lint()
finish,
})
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
lint.linters.swiftlint = { cmd = “swiftlint”, stdin = true, args = { “lint”, “–use-stdin”, “–config”, selectedSwiftlintConfig or os.getenv(“HOME”) .. “/.config/nvim/.swiftlint.yml”, — change path if wanted “-“, }, stream = “stdout”, ignore_exitcode = true, parser = require(“lint.parser”).from_pattern(sample, teams, severity_map, defaults), }
vim.api.nvim_create_autocmd({ “BufWritePost”, “BufReadPost”, “InsertLeave”, “TextChanged” }, { group = lint_augroup, callback = operate() require(“lint”).try_lint() finish, }) |
This fashion the content material of the buffer shall be handed to the linter. Nonetheless, the linter doesn’t know what file it’s, and in consequence, it’s unable to exclude any recordsdata.
SwiftFormat
For formatting, we are able to use conform.nvim plugin. It gives by default SwiftFormat integration, however we wish to enhance it by setting our personal configuration.
In our setup
operate we’ll add trying to find the undertaking configuration file, and a operate to format solely chosen vary.
— discover .swiftformat config file within the working listing
— might be simplified should you maintain it at all times within the root listing
native swiftFormatConfigs =
vim.fn.systemlist({ “discover”, vim.fn.getcwd(), “-iname”, “.swiftformat”, “-not”, “-path”, “*/.*/*” })
desk.kind(swiftFormatConfigs, operate(a, b)
return a ~= “” and #a < #b
finish)
native selectedSwiftFormatConfig
if swiftFormatConfigs[1] then
selectedSwiftFormatConfig = string.match(swiftFormatConfigs[1], “^%s*(.-)%s*$”)
finish
conform.setup({
formatters_by_ft = {
swift = { “swiftformat_ext” },
},
format_on_save = operate(bufnr)
return { timeout_ms = 500, lsp_fallback = true }
finish,
log_level = vim.log.ranges.ERROR,
formatters = {
swiftformat_ext = {
command = “swiftformat”,
args = {
“–config”,
selectedSwiftFormatConfig or “~/.config/nvim/.swiftformat”, — replace fallback path if wanted
“–stdinpath”,
“$FILENAME”,
},
range_args = operate(ctx)
return {
“–config”,
selectedSwiftFormatConfig or “~/.config/nvim/.swiftformat”, — replace fallback path if wanted
“–linerange”,
ctx.vary.begin[1] .. “,” .. ctx.vary[“end”][1],
}
finish,
stdin = true,
situation = operate(ctx)
return vim.fs.basename(ctx.filename) ~= “README.md”
finish,
},
},
})
vim.keymap.set({ “n”, “v” }, “<chief>mp”, operate()
conform.format({
lsp_fallback = true,
async = false,
timeout_ms = 500,
})
finish, { desc = “Format file or vary (in visible mode)” })
finish,
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
return { “stevearc/conform.nvim”, occasion = { “BufReadPre”, “BufNewFile” }, config = operate() native conform = require(“conform”)
— discover .swiftformat config file within the working listing — might be simplified should you maintain it at all times within the root listing native swiftFormatConfigs = vim.fn.systemlist({ “discover”, vim.fn.getcwd(), “-iname”, “.swiftformat”, “-not”, “-path”, “*/.*/*” })
desk.kind(swiftFormatConfigs, operate(a, b) return a ~= “” and #a < #b finish)
native selectedSwiftFormatConfig if swiftFormatConfigs[1] then selectedSwiftFormatConfig = string.match(swiftFormatConfigs[1], “^%s*(.-)%s*$”) finish
conform.setup({ formatters_by_ft = { swift = { “swiftformat_ext” }, }, format_on_save = operate(bufnr) return { timeout_ms = 500, lsp_fallback = true } finish, log_level = vim.log.ranges.ERROR, formatters = { swiftformat_ext = { command = “swiftformat”, args = { “–config”, selectedSwiftFormatConfig or “~/.config/nvim/.swiftformat”, — replace fallback path if wanted “–stdinpath”, “$FILENAME”, }, range_args = operate(ctx) return { “–config”, selectedSwiftFormatConfig or “~/.config/nvim/.swiftformat”, — replace fallback path if wanted “–linerange”, ctx.vary.begin[1] .. “,” .. ctx.vary[“end”][1], } finish, stdin = true, situation = operate(ctx) return vim.fs.basename(ctx.filename) ~= “README.md” finish, }, }, })
vim.keymap.set({ “n”, “v” }, “<chief>mp”, operate() conform.format({ lsp_fallback = true, async = false, timeout_ms = 500, }) finish, { desc = “Format file or vary (in visible mode)” }) finish, } |
Abstract
Strive it out! Now linting and formatting of Swift recordsdata ought to work appropriately.
If you happen to encounter any issues, ensure that the trail to the config file is appropriate. You’ll be able to attempt first with an express path.
The Holy Grail – Construct, Run & Check
How may you develop apps with out operating them? Everyone knows that many operations are doable utilizing xcodebuild
command line device and xcrun simctl
. Nonetheless, there is no such thing as a integration for Neovim to try this out of the field.
I began questioning how I may obtain that. First, I needed to create a number of easy instructions to construct and run functions. I began including them piece by piece and very quickly I ended up growing my plugin to do all of it for you! I known as it xcodebuild.nvim.
The plugin not solely means that you can construct and run functions, nevertheless it additionally gives a sophisticated log parser to generate a easy abstract, and plenty of extra actions like switching simulators, uninstalling apps, choosing schemes, and so on.
Nonetheless, probably the most attention-grabbing is the mixing with exams. The plugin is able to displaying you take a look at outcomes equally to Xcode with an icon subsequent to every take a look at. Moreover, it exhibits take a look at length and provides all issues and failed asserts to the QuickFix listing. On prime of that, you’ll be able to even choose exams in Visible mode and run solely these.
The supplied options cowl many of the actions that you’d anticipate from IDE.
Xcodebuild.nvim
The mixing is pretty easy and doesn’t require any extra steps.
vim.keymap.set(“n”, “<chief>xl”, “<cmd>XcodebuildToggleLogs<cr>”, { desc = “Toggle Xcodebuild Logs” })
vim.keymap.set(“n”, “<chief>xb”, “<cmd>XcodebuildBuild<cr>”, { desc = “Construct Undertaking” })
vim.keymap.set(“n”, “<chief>xr”, “<cmd>XcodebuildBuildRun<cr>”, { desc = “Construct & Run Undertaking” })
vim.keymap.set(“n”, “<chief>xt”, “<cmd>XcodebuildTest<cr>”, { desc = “Run Exams” })
vim.keymap.set(“n”, “<chief>xT”, “<cmd>XcodebuildTestClass<cr>”, { desc = “Run This Check Class” })
vim.keymap.set(“n”, “<chief>X”, “<cmd>XcodebuildPicker<cr>”, { desc = “Present All Xcodebuild Actions” })
vim.keymap.set(“n”, “<chief>xd”, “<cmd>XcodebuildSelectDevice<cr>”, { desc = “Choose Machine” })
vim.keymap.set(“n”, “<chief>xp”, “<cmd>XcodebuildSelectTestPlan<cr>”, { desc = “Choose Check Plan” })
vim.keymap.set(“n”, “<chief>xq”, “<cmd>Telescope quickfix<cr>”, { desc = “Present QuickFix Record” })
finish,
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
return { “wojciech-kulik/xcodebuild.nvim”, dependencies = { “nvim-telescope/telescope.nvim” }, config = operate() require(“xcodebuild”).setup()
vim.keymap.set(“n”, “<chief>xl”, “<cmd>XcodebuildToggleLogs<cr>”, { desc = “Toggle Xcodebuild Logs” }) vim.keymap.set(“n”, “<chief>xb”, “<cmd>XcodebuildBuild<cr>”, { desc = “Construct Undertaking” }) vim.keymap.set(“n”, “<chief>xr”, “<cmd>XcodebuildBuildRun<cr>”, { desc = “Construct & Run Undertaking” }) vim.keymap.set(“n”, “<chief>xt”, “<cmd>XcodebuildTest<cr>”, { desc = “Run Exams” }) vim.keymap.set(“n”, “<chief>xT”, “<cmd>XcodebuildTestClass<cr>”, { desc = “Run This Check Class” }) vim.keymap.set(“n”, “<chief>X”, “<cmd>XcodebuildPicker<cr>”, { desc = “Present All Xcodebuild Actions” }) vim.keymap.set(“n”, “<chief>xd”, “<cmd>XcodebuildSelectDevice<cr>”, { desc = “Choose Machine” }) vim.keymap.set(“n”, “<chief>xp”, “<cmd>XcodebuildSelectTestPlan<cr>”, { desc = “Choose Check Plan” }) vim.keymap.set(“n”, “<chief>xq”, “<cmd>Telescope quickfix<cr>”, { desc = “Present QuickFix Record” }) finish, } |
By default, the plugin makes use of xcbeautify
to format logs. You’ll be able to set up it utilizing Homebrew or you’ll be able to change it to one thing else or disable the formatter fully within the setup operate:
return { “wojciech-kulik/xcodebuild.nvim”, dependencies = { “nvim-telescope/telescope.nvim” }, config = operate() require(“xcodebuild”).setup({ logs = { logs_formatter = nil, }, }) finish, } |
You can begin the plugin by calling XcodebuildPicker
command to configure the undertaking and choose
some actions. For extra particulars, please see README.md.
The Final Step – Debugging
We’re virtually there. You’ll be able to’t develop apps with out a correctly working debugger. It’s a must have. Happily, the Neovim neighborhood has an answer for that as properly.
lldb / codelldb
To debug iOS functions you’ll be able to join the debugger to your app course of through the use of lldb
command line debugger included in Xcode. Nonetheless, we’ll want one thing extra to attach it with our Neovim.
Here’s a comparable state of affairs to the one with autocompletion. It will be a nightmare to supply a customized integration by every IDE for every language. Due to this fact, Debug Adapter Protocol (DAP) has been launched, once more by Microsoft, to unify that.
In fact, Apple doesn’t present any integration with DAP. Nonetheless, the neighborhood involves the rescue another time they usually constructed a device known as codelldb. It may possibly debug not solely Swift but in addition C++, Rust, Fortran, Kotlin Native, Nim, Goal-C, Pascal, and Zig.
It’s a Visible Studio Code plugin however we are able to use it with Neovim as properly. First, obtain the newest launch for DARWIN structure from HERE and UNZIP the vsix
file to the situation the place you wish to maintain it.
nvim-dap
The most well-liked plugin for debugging is nvim-dap. You’ll be able to simply combine it with codelldb to supply assist for iOS and macOS apps.
To keep away from guide work with constructing, operating and attaching, my plugin xcodebuild.nvim gives additionally some serving to capabilities for integration with nvim-dap
. Simply take a look at the code under and replace paths.
dap.configurations.swift = {
{
title = “iOS App Debugger”,
sort = “codelldb”,
request = “connect”,
program = xcodebuild.get_program_path,
cwd = “${workspaceFolder}”,
stopOnEntry = false,
waitFor = true,
},
}
dap.adapters.codelldb = {
sort = “server”,
port = “13000”,
executable = {
— TODO: be certain that to set path to your codelldb
command = os.getenv(“HOME”) .. “/Downloads/codelldb-aarch64-darwin/extension/adapter/codelldb”,
args = {
“–port”,
“13000”,
“–liblldb”,
— TODO: ensure that this path is appropriate in your machine
“/Purposes/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Variations/A/LLDB”,
},
},
}
— good breakpoint icons
native outline = vim.fn.sign_define
outline(“DapBreakpoint”, { textual content = “”, texthl = “DiagnosticError”, linehl = “”, numhl = “” })
outline(“DapBreakpointRejected”, { textual content = “”, texthl = “DiagnosticError”, linehl = “”, numhl = “” })
outline(“DapStopped”, { textual content = “”, texthl = “DiagnosticOk”, linehl = “”, numhl = “” })
outline(“DapLogPoint”, { textual content = “”, texthl = “DiagnosticInfo”, linehl = “”, numhl = “” })
outline(“DapLogPoint”, { textual content = “”, texthl = “DiagnosticInfo”, linehl = “”, numhl = “” })
— integration with xcodebuild.nvim
vim.keymap.set(“n”, “<chief>dd”, xcodebuild.build_and_debug, { desc = “Construct & Debug” })
vim.keymap.set(“n”, “<chief>dr”, xcodebuild.debug_without_build, { desc = “Debug With out Constructing” })
vim.keymap.set(“n”, “<chief>dc”, dap.proceed)
vim.keymap.set(“n”, “<chief>ds”, dap.step_over)
vim.keymap.set(“n”, “<chief>di”, dap.step_into)
vim.keymap.set(“n”, “<chief>do”, dap.step_out)
vim.keymap.set(“n”, “<C-b>”, dap.toggle_breakpoint)
vim.keymap.set(“n”, “<C-s-b>”, operate()
dap.set_breakpoint(nil, nil, vim.fn.enter(“Log level message: “))
finish)
vim.keymap.set(“n”, “<Chief>dx”, operate()
dap.terminate()
require(“xcodebuild.actions”).cancel()
native success, dapui = pcall(require, “dapui”)
if success then
dapui.shut()
finish
finish)
finish,
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
return { “mfussenegger/nvim-dap”, dependencies = { “wojciech-kulik/xcodebuild.nvim”, }, config = operate() native dap = require(“dap”) native xcodebuild = require(“xcodebuild.dap”)
dap.configurations.swift = { { title = “iOS App Debugger”, sort = “codelldb”, request = “connect”, program = xcodebuild.get_program_path, cwd = “${workspaceFolder}”, stopOnEntry = false, waitFor = true, }, }
dap.adapters.codelldb = { sort = “server”, port = “13000”, executable = { — TODO: be certain that to set path to your codelldb command = os.getenv(“HOME”) .. “/Downloads/codelldb-aarch64-darwin/extension/adapter/codelldb”, args = { “–port”, “13000”, “–liblldb”, — TODO: ensure that this path is appropriate in your machine “/Purposes/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Variations/A/LLDB”, }, }, }
— good breakpoint icons native outline = vim.fn.sign_define outline(“DapBreakpoint”, { textual content = “”, texthl = “DiagnosticError”, linehl = “”, numhl = “” }) outline(“DapBreakpointRejected”, { textual content = “”, texthl = “DiagnosticError”, linehl = “”, numhl = “” }) outline(“DapStopped”, { textual content = “”, texthl = “DiagnosticOk”, linehl = “”, numhl = “” }) outline(“DapLogPoint”, { textual content = “”, texthl = “DiagnosticInfo”, linehl = “”, numhl = “” }) outline(“DapLogPoint”, { textual content = “”, texthl = “DiagnosticInfo”, linehl = “”, numhl = “” })
— integration with xcodebuild.nvim vim.keymap.set(“n”, “<chief>dd”, xcodebuild.build_and_debug, { desc = “Construct & Debug” }) vim.keymap.set(“n”, “<chief>dr”, xcodebuild.debug_without_build, { desc = “Debug With out Constructing” })
vim.keymap.set(“n”, “<chief>dc”, dap.proceed) vim.keymap.set(“n”, “<chief>ds”, dap.step_over) vim.keymap.set(“n”, “<chief>di”, dap.step_into) vim.keymap.set(“n”, “<chief>do”, dap.step_out) vim.keymap.set(“n”, “<C-b>”, dap.toggle_breakpoint) vim.keymap.set(“n”, “<C-s-b>”, operate() dap.set_breakpoint(nil, nil, vim.fn.enter(“Log level message: “)) finish) vim.keymap.set(“n”, “<Chief>dx”, operate() dap.terminate() require(“xcodebuild.actions”).cancel()
native success, dapui = pcall(require, “dapui”) if success then dapui.shut() finish finish) finish, } |
nvim-dap-ui
To get a extra acquainted debugging expertise you’ll need nvim-dap-ui. It’s an extension for nvim-dap
that may present you routinely all panels essential for debugging. That is one other must-have.
You’ll be able to simply modify the format, icons, and modify debugging mode to your wants. The mixing may be very easy.
native dap, dapui = require(“dap”), require(“dapui”)
dap.listeners.after.event_initialized[“dapui_config”] = operate()
dapui.open()
finish
dap.listeners.earlier than.event_terminated[“dapui_config”] = operate()
dapui.shut()
finish
dap.listeners.earlier than.event_exited[“dapui_config”] = operate()
dapui.shut()
finish
finish,
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
return { “rcarriga/nvim-dap-ui”, dependencies = { “mfussenegger/nvim-dap”, }, lazy = true, config = operate() require(“dapui”).setup({ controls = { factor = “repl”, enabled = true, }, floating = { border = “single”, mappings = { shut = { “q”, “<Esc>” }, }, }, icons = { collapsed = “”, expanded = “”, current_frame = “” }, layouts = { { parts = { { id = “stacks”, measurement = 0.25 }, { id = “scopes”, measurement = 0.25 }, { id = “breakpoints”, measurement = 0.25 }, { id = “watches”, measurement = 0.25 }, }, place = “left”, measurement = 60, }, { parts = { { id = “repl”, measurement = 1.0 }, — { id = “console”, measurement = 0.5 }, }, place = “backside”, measurement = 10, }, }, })
native dap, dapui = require(“dap”), require(“dapui”)
dap.listeners.after.event_initialized[“dapui_config”] = operate() dapui.open() finish dap.listeners.earlier than.event_terminated[“dapui_config”] = operate() dapui.shut() finish dap.listeners.earlier than.event_exited[“dapui_config”] = operate() dapui.shut() finish finish, } |
Abstract
Now you must be capable of construct, run, and debug the app by merely hitting <chief>dd
. As soon as the app is operating the debugger must be routinely connected.
Bonus: prints
The debugger doesn’t know something about Swift print
in your apps. That is one thing added by Xcode. If you wish to see your print logs you’ll be able to set a breakpoint in your logger and log the message.
Utilizing my configuration you’ll be able to press Management-Shift-B
and sort the title of the variable that incorporates the message like: {message}
and that’s it. Now the debugger will print your logs with out stopping.
Bonus 2: Persisting Breakpoints
By default nvim-dap
doesn’t retailer your breakpoints, however you’ll be able to fairly simply add this performance. Simply add on the prime of your nvim-dap
configuration file the next code:
native M = {}
native operate file_exist(file_path)
native f = io.open(file_path, “r”)
return f ~= nil and io.shut(f)
finish
operate M.retailer()
native settings = vim.fn.getcwd() .. “/.nvim”
native breakpoints_fp = settings .. “/breakpoints.json”
vim.fn.systemlist({ “mkdir”, “-p”, settings })
native bps = {}
if file_exist(breakpoints_fp) then
native breakpoints_handle = io.open(breakpoints_fp)
if breakpoints_handle then
native load_bps_raw = breakpoints_handle:learn(“*a”)
breakpoints_handle:shut()
if string.len(load_bps_raw) > 0 then
bps = vim.fn.json_decode(load_bps_raw)
finish
finish
finish
native breakpoints_by_buf = breakpoints.get()
for _, bufrn in ipairs(vim.api.nvim_list_bufs()) do
bps[vim.api.nvim_buf_get_name(bufrn)] = breakpoints_by_buf[bufrn]
finish
native fp = io.open(breakpoints_fp, “w”)
if fp then
fp:write(vim.fn.json_encode(bps))
fp:shut()
finish
finish
operate M.load()
native settings = vim.fn.getcwd() .. “/.nvim”
native fp = io.open(settings .. “/breakpoints.json”, “r”)
if not fp then
return
finish
native content material = fp:learn(“*a”)
fp:shut()
if string.len(content material) == 0 then
return
finish
native bps = vim.fn.json_decode(content material)
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
native file_name = vim.api.nvim_buf_get_name(buf)
if bps[file_name] then
for _, bp in pairs(bps[file_name]) do
native opts = {
situation = bp.situation,
log_message = bp.logMessage,
hit_condition = bp.hitCondition,
}
breakpoints.set(opts, tonumber(buf), bp.line)
finish
finish
finish
finish
return {
— setup
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
native breakpoints = require(“dap.breakpoints”)
native M = {}
native operate file_exist(file_path) native f = io.open(file_path, “r”) return f ~= nil and io.shut(f) finish
operate M.retailer() native settings = vim.fn.getcwd() .. “/.nvim” native breakpoints_fp = settings .. “/breakpoints.json” vim.fn.systemlist({ “mkdir”, “-p”, settings })
native bps = {}
if file_exist(breakpoints_fp) then native breakpoints_handle = io.open(breakpoints_fp)
if breakpoints_handle then native load_bps_raw = breakpoints_handle:learn(“*a”) breakpoints_handle:shut()
if string.len(load_bps_raw) > 0 then bps = vim.fn.json_decode(load_bps_raw) finish finish finish
native breakpoints_by_buf = breakpoints.get() for _, bufrn in ipairs(vim.api.nvim_list_bufs()) do bps[vim.api.nvim_buf_get_name(bufrn)] = breakpoints_by_buf[bufrn] finish
native fp = io.open(breakpoints_fp, “w”) if fp then fp:write(vim.fn.json_encode(bps)) fp:shut() finish finish
operate M.load() native settings = vim.fn.getcwd() .. “/.nvim”
native fp = io.open(settings .. “/breakpoints.json”, “r”) if not fp then return finish
native content material = fp:learn(“*a”) fp:shut()
if string.len(content material) == 0 then return finish
native bps = vim.fn.json_decode(content material)
for _, buf in ipairs(vim.api.nvim_list_bufs()) do native file_name = vim.api.nvim_buf_get_name(buf)
if bps[file_name] then for _, bp in pairs(bps[file_name]) do native opts = { situation = bp.situation, log_message = bp.logMessage, hit_condition = bp.hitCondition, } breakpoints.set(opts, tonumber(buf), bp.line) finish finish finish finish
return { — setup } |
Now, you’ll be able to replace your keymaps to save lots of breakpoints:
vim.keymap.set(“n”, “<C-b>”, operate() dap.toggle_breakpoint() M.retailer() finish) vim.keymap.set(“n”, “<C-s-b>”, operate() dap.set_breakpoint(nil, nil, vim.fn.enter(“Log level message: “)) M.retailer() finish |
The very last thing is to load them when Neovim begins. Put it under the keymaps:
vim.api.nvim_create_autocmd({ “VimEnter” }, {
group = autogroup,
sample = “*”,
as soon as = true,
callback = operate()
vim.defer_fn(M.load, 500)
finish,
})
native autogroup = vim.api.nvim_create_augroup(“dap-breakpoints”, { clear = true })
vim.api.nvim_create_autocmd({ “VimEnter” }, { group = autogroup, sample = “*”, as soon as = true, callback = operate() vim.defer_fn(M.load, 500) finish, }) |
Full Configuration
I ready a pattern config utilizing LazyVim. It covers all of the options offered on this article. If you wish to attempt it out, take a look at this repository: ios-dev-starter-nvim. Be certain to put in all essential dependencies.
Ultimate Phrases
Hell has frozen over! Utilizing the strategy described above you’ll barely must open Xcode to develop apps.
It took me a number of months, step-by-step, to get up to now. Ultimately, I needed to take issues into my very own fingers and implement xcodebuild.nvim plugin, however the journey was value it!
If you happen to adopted this information, you must find yourself with a working code completion, linting, formatting, debugger, and primary actions to check, construct, run, and deploy apps to simulators. All collectively covers not less than 90% of the event time.
I’ve been utilizing this strategy for a while and I’m very pleased with the outcomes. Neovim works nice and I’m not a prisoner of dumb Xcode limitations anymore. I don’t have to attend one other 5 years to get a reliably working rename, to get fuzzy search, or to get “dot” assist in Vim mode.
There are numerous third-party dependencies right here, so chances are you’ll face some issues sooner or later. This text was written in a means that ought to let you copy part by part with none additional steps. Nonetheless, should you encounter difficulties, or if one thing is unclear, be happy to submit a remark.
Neovim is a really extensible device, you’ll be able to simply configure or add something you want. As you’ll be able to see, I used to be in a position to create a posh plugin offering take a look at integration just like the one in Xcode and all of it took solely round 2 weeks (and is almost certainly extra dependable than the one in Xcode).
All the most effective within the new period of iOS & macOS growth outdoors of Xcode.
Enhance Your Work
Psst! If you wish to enhance your productiveness even additional, take a look at this app.
Snippety is a device that may make every day duties extra gratifying by offering fast entry to your snippets. Snippety works flawlessly with each textual content area! Simply press ⌘⇧House, discover your snippet, and hit ↩︎. You’ll be able to outline additionally your key phrases and use snippets by simply typing with out even opening the app!