A plugin that lets you move your iOS, iPadOS and macOS apps development to Neovim. It supports most of Xcode actions that are required to work with a project, including device selection, building, launching, and testing.
This plugin is in the early stage of development. It was tested on a limited number of projects and configurations. Therefore, it still could be buggy. If you find any issue don't hesitate to fix it and create a pull request or just report it.
I've been looking for a solution to move my development to any other IDE than Xcode for a long time. It seems that this plugin + nvim-dap + nvim-dap-ui + nvim-lspconfig + xcode-build-server, all together, provide everything that is needed to move to Neovim with iOS, iPadOS, and macOS apps development.
Of course, you will still need Xcode for some project setup & management. Also, you may need to migrate to tuist or xcodegen to be able to add new files easily.
It is also my first Neovim plugin. Hopefully, a good one ๐.
- Support for iOS, iPadOS, and macOS apps.
- Project-based configuration.
- Configuration wizard to setup: project file, scheme, config, device, and test plan.
- Built based on core command line tools like
xcodebuild
andxcrun simctl
. It doesn't require any external tools, onlyxcbeautify
to format logs, but it could be changed in configuration. - Build, run and test actions.
- Test Explorer to visually present all tests and results.
- App deployment to selected iOS simulator.
- Uninstall mobile app.
- Running only selected tests (one test, one class, selected tests in visual mode, whole test plan).
- Showing icons with test result next to each test.
- Showing test duration next to each test.
- Showing test errors in diagnostics and on the quickfix list.
- Showing build errors and warnings on the quickfix list.
- Showing build progress bar based on the previous build time.
- Showing code coverage.
- Showing preview of failed snapshot tests (if you use swift-snapshot-testing)
- Advanced log parser to detect all errors, warnings, and failing tests and to present them nicely formatted.
- Auto saving files before build or test actions.
- nvim-dap helper functions to let you easily build, run, and attach the debugger.
- lualine.nvim integration to show current device and project settings.
- telescope.nvim integration to show pickers with selectable project options.
- Picker with all available actions.
- Neovim (not sure which version, use the latest one ๐ ).
- telescope.nvim used to present pickers by the plugin.
- nui.nvim used to present code coverage report.
- xcbeautify - Xcode logs formatter (optional - you can set a different tool or disable formatting in the config).
- Xcode (make sure that
xcodebuild
andxcrun simctl
work correctly). - To get the best experience with apps development, you should install and configure nvim-dap and nvim-dap-ui to be able to debug.
- This plugin requires the project to be written in Swift. It was tested only with Xcode 15.
- Make sure to configure LSP properly for iOS/macOS apps. You can read how to do that in my post: The Complete Guide To iOS & macOS Development In Neovim.
Install tools:
brew install xcbeautify
Install the plugin using your preferred package manager:
๐ค lazy.nvim
return {
"wojciech-kulik/xcodebuild.nvim",
dependencies = {
"nvim-telescope/telescope.nvim",
"MunifTanjim/nui.nvim",
},
config = function()
require("xcodebuild").setup({
-- put some options here or leave it empty to use default settings
})
end,
}
See default Xcodebuild.nvim config
{
restore_on_start = true, -- logs, diagnostics, and marks will be loaded on VimEnter (may affect performance)
auto_save = true, -- save all buffers before running build or tests (command: silent wa!)
show_build_progress_bar = true, -- shows [ ... ] progress bar during build, based on the last duration
prepare_snapshot_test_previews = true, -- prepares a list with failing snapshot tests
test_search = {
file_matching = "filename_lsp", -- one of: filename, lsp, lsp_filename, filename_lsp. Check out README for details
target_matching = true, -- checks if the test file target matches the one from logs. Try disabling it in case of not showing test results
lsp_client = "sourcekit", -- name of your LSP for Swift files
lsp_timeout = 200, -- LSP timeout in milliseconds
},
commands = {
cache_devices = true, -- cache recently loaded devices. Restart Neovim to clean cache.
extra_build_args = "-parallelizeTargets", -- extra arguments for `xcodebuild build`
extra_test_args = "-parallelizeTargets", -- extra arguments for `xcodebuild test`
project_search_max_depth = 3, -- maxdepth of xcodeproj/xcworkspace search while using configuration wizard
},
logs = {
auto_open_on_success_tests = false, -- open logs when tests succeeded
auto_open_on_failed_tests = false, -- open logs when tests failed
auto_open_on_success_build = false, -- open logs when build succeeded
auto_open_on_failed_build = true, -- open logs when build failed
auto_close_on_app_launch = false, -- close logs when app is launched
auto_close_on_success_build = false, -- close logs when build succeeded (only if auto_open_on_success_build=false)
auto_focus = true, -- focus logs buffer when opened
filetype = "objc", -- file type set for buffer with logs
open_command = "silent botright 20split {path}", -- command used to open logs panel. You must use {path} variable to load the log file
logs_formatter = "xcbeautify --disable-colored-output", -- command used to format logs, you can use "" to skip formatting
only_summary = false, -- if true logs won't be displayed, just xcodebuild.nvim summary
show_warnings = true, -- show warnings in logs summary
notify = function(message, severity) -- function to show notifications from this module (like "Build Failed")
vim.notify(message, severity)
end,
notify_progress = function(message) -- function to show live progress (like during tests)
vim.cmd("echo '" .. message .. "'")
end,
},
marks = {
show_signs = true, -- show each test result on the side bar
success_sign = "โ", -- passed test icon
failure_sign = "โ", -- failed test icon
show_test_duration = true, -- show each test duration next to its declaration
show_diagnostics = true, -- add test failures to diagnostics
file_pattern = "*Tests.swift", -- test diagnostics will be loaded in files matching this pattern (if available)
},
quickfix = {
show_errors_on_quickfixlist = true, -- add build/test errors to quickfix list
show_warnings_on_quickfixlist = true, -- add build warnings to quickfix list
},
test_explorer = {
enabled = true, -- enable Test Explorer
auto_open = true, -- opens Test Explorer when tests are started
open_command = "botright 42vsplit Test Explorer", -- command used to open Test Explorer
success_sign = "โ", -- passed test icon
failure_sign = "โ", -- failed test icon
progress_sign = "โฆ", -- progress icon (only used when animate_status=false)
disabled_sign = "โธ", -- disabled test icon
partial_execution_sign = "โ", -- icon for a class or target when only some tests were executed
not_executed_sign = " ", -- not executed or partially executed test icon
show_disabled_tests = false, -- show disabled tests
animate_status = true, -- animate status while running tests
cursor_follows_tests = true, -- moves cursor to the last test executed
},
code_coverage = {
enabled = false, -- generate code coverage report and show marks
file_pattern = "*.swift", -- coverage will be shown in files matching this pattern
-- configuration of line coverage presentation:
covered_sign = "",
partially_covered_sign = "โ",
not_covered_sign = "โ",
not_executable_sign = "",
},
code_coverage_report = {
warning_coverage_level = 60,
error_coverage_level = 30,
open_expanded = false,
},
highlights = {
-- you can override here any highlight group used by this plugin
-- simple color: XcodebuildCoverageReportOk = "#00ff00",
-- link highlights: XcodebuildCoverageReportOk = "DiagnosticOk",
-- full customization: XcodebuildCoverageReportOk = { fg = "#00ff00", bold = true },
},
}
See all highlights
Highlight Group | Description |
---|---|
XcodebuildTestSuccessSign |
Test passed sign |
XcodebuildTestFailureSign |
Test failed sign |
XcodebuildTestSuccessDurationSign |
Test duration of a passed test |
XcodebuildTestFailureDurationSign |
Test duration of a failed test |
Highlight Group | Description |
---|---|
XcodebuildTestExplorerTest |
Test name (function) |
XcodebuildTestExplorerClass |
Test class |
XcodebuildTestExplorerTarget |
Test target |
XcodebuildTestExplorerTestInProgress |
Test in progress sign |
XcodebuildTestExplorerTestPassed |
Test passed sign |
XcodebuildTestExplorerTestFailed |
Test failed sign |
XcodebuildTestExplorerTestDisabled |
Test disabled sign |
XcodebuildTestExplorerTestNotExecuted |
Test not executed sign |
XcodebuildTestExplorerTestPartialExecution |
Not all tests executed sign |
Highlight Group | Description |
---|---|
XcodebuildCoverageFullSign |
Covered line - sign |
XcodebuildCoverageFullNumber |
Covered line - line number |
XcodebuildCoverageFullLine |
Covered line - code |
XcodebuildCoveragePartialSign |
Partially covered line - sign |
XcodebuildCoveragePartialNumber |
Partially covered line - line number |
XcodebuildCoveragePartialLine |
Partially covered line - code |
XcodebuildCoverageNoneSign |
Not covered line - sign |
XcodebuildCoverageNoneNumber |
Not covered line - line number |
XcodebuildCoverageNoneLine |
Not covered line - code |
XcodebuildCoverageNotExecutableSign |
Not executable line - sign |
XcodebuildCoverageNotExecutableNumber |
Not executable line - line number |
XcodebuildCoverageNotExecutableLine |
Not executable line - code |
Highlight Group | Description |
---|---|
XcodebuildCoverageReportOk |
Percentage color when above warning_coverage_level |
XcodebuildCoverageReportWarning |
Percentage color when below warning_coverage_level |
XcodebuildCoverageReportError |
Percentage color when below error_coverage_level |
See all strategies
xcodebuild
logs provide the following information about the test: target, test class, and test name. The plugin needs to find the file location based on that, which is not a trivial task.
In order to support multiple cases, the plugin allows you to choose the search mode. It offers four modes to find a test class. You can change it by setting test_search.file_matching
.
filename
- it assumes that the test class name matches the file name. It finds matching files and then based on the build output, it checks whether the file belongs to the desired target.lsp
- it uses LSP to find the class symbol. Each match is checked if it belongs to the desired target.filename_lsp
first tryfilename
mode, if it fails trylsp
mode.lsp_filename
first trylsp
mode, if it fails tryfilename
mode.
filename_lsp
is the recommended mode, because filename
search is faster than lsp
, but you also have lsp
fallback if there is no match from filename
.
๐ If you notice that your test results don't appear or appear in incorrect files, try playing with these modes.
๐ If your test results don't appear, you can also try disabling test_search.target_matching
. This way the plugin will always use the first match without checking its target.
Important
I wrote an article that sums up all steps to set up your Neovim from scratch to develop iOS and macOS apps:
The Complete Guide To iOS & macOS Development In Neovim
You can also check out a sample Neovim configuration that I prepared for iOS development: ios-dev-starter-nvim
This plugin supports only iOS and macOS applications. However, if you develop Swift Package for one of those platforms, you can easily use this plugin by creating a sample iOS/macOS project in your root directory and adding your package as a dependency.
nvim-dap plugin lets you debug applications like in any other IDE. On top of that nvim-dap-ui extension will present for you all panels with stack, breakpoints, variables, logs, etc.
See nvim-dap configuration
To configure DAP for development:
- Download codelldb VS Code plugin from: HERE. For macOS use
darwin
version. Just unzipvsix
file and set paths below. - Install also nvim-dap-ui for a nice GUI to debug.
return {
"mfussenegger/nvim-dap",
dependencies = {
"wojciech-kulik/xcodebuild.nvim"
},
config = function()
local dap = require("dap")
local xcodebuild = require("xcodebuild.dap")
dap.configurations.swift = {
{
name = "iOS App Debugger",
type = "codelldb",
request = "attach",
program = xcodebuild.get_program_path,
-- alternatively, you can wait for the process manually
-- pid = xcodebuild.wait_for_pid,
cwd = "${workspaceFolder}",
stopOnEntry = false,
waitFor = true,
},
}
dap.adapters.codelldb = {
type = "server",
port = "13000",
executable = {
-- set path to the downloaded codelldb
-- sample path: "/Users/YOU/Downloads/codelldb-aarch64-darwin/extension/adapter/codelldb"
command = "/path/to/codelldb/extension/adapter/codelldb",
args = {
"--port",
"13000",
"--liblldb",
-- make sure that this path is correct on your side
"/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB",
},
},
}
-- disables annoying warning that requires hitting enter
local orig_notify = require("dap.utils").notify
require("dap.utils").notify = function(msg, log_level)
if not string.find(msg, "Either the adapter is slow") then
orig_notify(msg, log_level)
end
end
-- sample keymaps to debug application
vim.keymap.set("n", "<leader>dd", xcodebuild.build_and_debug, { desc = "Build & Debug" })
vim.keymap.set("n", "<leader>dr", xcodebuild.debug_without_build, { desc = "Debug Without Building" })
end,
}
Important
Make sure to open your project's root directory in Neovim and run XcodebuildSetup
to configure the project. The plugin needs several information like project file, scheme, config, device, and test plan to be able to run commands.
๐ See all user commands
Xcodebuild.nvim comes with the following commands:
Command | Description |
---|---|
XcodebuildSetup |
Run configuration wizard to select project configuration |
XcodebuildPicker |
Show picker with all available actions |
XcodebuildBuild |
Build project |
XcodebuildCleanBuild |
Build project (clean build) |
XcodebuildBuildRun |
Build & run app |
XcodebuildBuildForTesting |
Build for testing |
XcodebuildRun |
Run app without building |
XcodebuildCancel |
Cancel currently running action |
XcodebuildCleanDerivedData |
Deletes project's DerivedData |
XcodebuildToggleLogs |
Toggle logs panel |
XcodebuildOpenLogs |
Open logs panel |
XcodebuildCloseLogs |
Close logs panel |
Command | Description |
---|---|
XcodebuildTest |
Run tests (whole test plan) |
XcodebuildTestTarget |
Run test target (where the cursor is) |
XcodebuildTestClass |
Run test class (where the cursor is) |
XcodebuildTestFunc |
Run test (where the cursor is) |
XcodebuildTestSelected |
Run selected tests (using visual mode) |
XcodebuildTestFailing |
Rerun previously failed tests |
XcodebuildFailingSnapshots |
Show a picker with failing snapshot tests |
Command | Description |
---|---|
XcodebuildToggleCodeCoverage |
Toggle code coverage marks on the side bar |
XcodebuildShowCodeCoverageReport |
Open HTML code coverage report |
XcodebuildJumpToNextCoverage |
Jump to next code coverage mark |
XcodebuildJumpToPrevCoverage |
Jump to previous code coverage mark |
Command | Description |
---|---|
XcodebuildTestExplorerShow |
Show Test Explorer |
XcodebuildTestExplorerHide |
Hide Test Explorer |
XcodebuildTestExplorerToggle |
Toggle Test Explorer |
XcodebuildTestExplorerRunSelectedTests |
Run Selected Tests |
XcodebuildTestExplorerRerunTests |
Re-run recently selected tests |
Command | Description |
---|---|
XcodebuildSelectProject |
Show project file picker |
XcodebuildSelectScheme |
Show scheme picker |
XcodebuildSelectConfig |
Show build configuration picker |
XcodebuildSelectDevice |
Show device picker |
XcodebuildSelectTestPlan |
Show test plan picker |
XcodebuildShowConfig |
Print current project configuration |
XcodebuildBootSimulator |
Boot selected simulator |
XcodebuildUninstall |
Uninstall mobile app |
-- Lua
vim.keymap.set("n", "<leader>X", "<cmd>XcodebuildPicker<cr>", { desc = "Show All Xcodebuild Actions" })
vim.keymap.set("n", "<leader>xl", "<cmd>XcodebuildToggleLogs<cr>", { desc = "Toggle Xcodebuild Logs" })
vim.keymap.set("n", "<leader>xb", "<cmd>XcodebuildBuild<cr>", { desc = "Build Project" })
vim.keymap.set("n", "<leader>xr", "<cmd>XcodebuildBuildRun<cr>", { desc = "Build & Run Project" })
vim.keymap.set("n", "<leader>xt", "<cmd>XcodebuildTest<cr>", { desc = "Run Tests" })
vim.keymap.set("v", "<leader>xt", "<cmd>XcodebuildTestSelected<cr>", { desc = "Run Selected Tests" })
vim.keymap.set("n", "<leader>xT", "<cmd>XcodebuildTestClass<cr>", { desc = "Run This Test Class" })
vim.keymap.set("n", "<leader>xf", "<cmd>XcodebuildTestTarget<cr>", { desc = "Run This Test Target" })
vim.keymap.set("n", "<leader>xd", "<cmd>XcodebuildSelectDevice<cr>", { desc = "Select Device" })
vim.keymap.set("n", "<leader>xp", "<cmd>XcodebuildSelectTestPlan<cr>", { desc = "Select Test Plan" })
vim.keymap.set("n", "<leader>xs", "<cmd>XcodebuildFailingSnapshots<cr>", { desc = "Show Failing Snapshots" })
vim.keymap.set("n", "<leader>xc", "<cmd>XcodebuildToggleCodeCoverage<cr>", { desc = "Toggle Code Coverage" })
vim.keymap.set("n", "<leader>xC", "<cmd>XcodebuildShowCodeCoverageReport<cr>", { desc = "Show Code Coverage Report" })
vim.keymap.set("n", "<leader>xe", "<cmd>XcodebuildTestExplorerToggle<cr>", { desc = "Toggle Test Explorer" })
vim.keymap.set("n", "[r", "<cmd>XcodebuildJumpToPrevCoverage<cr>", { desc = "Jump To Previous Coverage" })
vim.keymap.set("n", "]r", "<cmd>XcodebuildJumpToNextCoverage<cr>", { desc = "Jump To Next Coverage" })
vim.keymap.set("n", "<leader>xq", "<cmd>Telescope quickfix<cr>", { desc = "Show QuickFix List" })
Tip
Press <leader>X
to access the picker with all commands.
- Press
o
on a failed test in the summary section to jump to the failing location - Press
q
to close the panel
- Press
o
to jump to the test implementation - Press
t
to run selected tests - Press
T
to re-run recently selected tests - Press
R
to reload test list - Press
[
to jump to the previous failed test - Press
]
to jump to the next failed test - Press
<cr>
to expand or collapse the current node - Press
<tab>
to expand or collapse all classes - Press
q
to close the Test Explorer
You can also integrate this plugin with lualine.nvim.
See Lualine configuration
lualine_x = {
{ "diff" },
{ "'๓ฐจ ' .. vim.g.xcodebuild_test_plan" },
{ "vim.g.xcodebuild_platform == 'macOS' and '๏ macOS' or '๏ ' .. vim.g.xcodebuild_device_name" },
{ "'๎ ' .. vim.g.xcodebuild_os" },
{ "encoding" },
{ "filetype", icon_only = true },
}
Global variables that you can use:
Variable | Description |
---|---|
vim.g.xcodebuild_device_name |
Device name (ex. iPhone 15 Pro) |
vim.g.xcodebuild_os |
OS version (ex. 16.4) |
vim.g.xcodebuild_platform |
Device platform (macOS or iPhone Simulator) |
vim.g.xcodebuild_config |
Selected build config (ex. Debug) |
vim.g.xcodebuild_scheme |
Selected project scheme (ex. MyApp) |
vim.g.xcodebuild_test_plan |
Selected Test Plan (ex. MyAppTests) |
See how to configure
Using xcodebuild.nvim you can also check the code coverage after running tests.- Make sure that you enabled code coverage for desired targets in your test plan.
- Enable code coverage in xcodebuild config:
code_coverage = {
enabled = true,
}
- Toggle code coverage
:XcodebuildToggleCodeCoverage
or:lua require("xcodebuild.actions").toggle_code_coverage(true)
. - Run tests - once it's finished, code coverage should appear on the sidebar with line numbers.
- You can jump between code coverage marks using
:XcodebuildJumpToPrevCoverage
and:XcodebuildJumpToNextCoverage
. - You can also check out the report using
:XcodebuildShowCodeCoverageReport
command.
The plugin sends XcodebuildCoverageToggled
event that you can use to disable other plugins presenting lines on the side bar (like gitsigns
). Example:
vim.api.nvim_create_autocmd("User", {
pattern = "XcodebuildCoverageToggled",
callback = function(event)
local isOn = event.data
require("gitsigns").toggle_signs(not isOn)
end,
})
Coverage Report Keys:
Key | Description |
---|---|
enter or tab |
Expand or collapse the current node |
o |
Open source file |
[!CAUTION] From time to time, the code coverage may fail or some targets may be missing (Xcode's bug). Try running tests again then.
If you run tests, modify file and toggle code coverage AFTER that, the placement of marks will be incorrect (because it doesn't know about changes that you made). However, if you show code coverage and after that you modify the code, marks will be moving while you are editing the file.
This plugin offers a nice list of failing snapshot tests. For each test it generates a preview image combining reference, failure, and difference images into one. It works with swift-snapshot-testing library.
Run :XcodebuildFailingSnapshots
to see the list.
If you want to use functions directly instead of user commands, then please see xcodebuild.actions module.
Loading project configuration is a very complex task that relies on parsing multiple crazy outputs from xcodebuild
commands. Those logs are a pure nightmare to parse. It may not always work. In case of any issues with that, you can try manually providing the configuration by adding .nvim/xcodebuild/settings.json
file in your root directory.
Sample settings.json
:
{
"platform": "iOS",
"testPlan": "UnitTests",
"config": "Debug",
"xcodeproj": "/path/to/project/App.xcodeproj",
"projectFile": "/path/to/project/App.xcworkspace",
"projectCommand": "-workspace '/path/to/project/App.xcworkspace'",
"bundleId": "com.company.bundle-id",
"destination": "00006000-000C58DC1ED8801E",
"productName": "App",
"scheme": "App",
"appPath": "/Users/YOU/Library/Developer/Xcode/DerivedData/App-abafsafasdfasdf/Build/Products/Debug/App.app"
}
platform
-macOS
oriOS
destination
- simulator IDprojectFile
/projectCommand
- can bexcodeproj
orxcworkspace
, the main project file that you use