使用 Neovim 和 OC 编写 iOS 应用

2023/04/21 posted in  Apple
Tags:  #Objective-C #Vim

背景

本人是 Vim 党,准确说是 Neovim 党,所以文本编辑尽量都用 nvim 完成,当然也就包括写代码啦。

很多 IDE 都有 Vim 模式插件(包括 Xcode),但说实话用得都不习惯,我宁愿同时开着 nvim 和 IDE,用 nvim 编写代码,用 IDE 做一些别的 nvim 不支持的操作。

最近我研究了一下如何更优雅的用 nvim 和 OC 编写 iOS 应用代码,让 nvim 支持 OC 代码补全、错误提示、代码跳转、格式化等功能。

代码补全、错误提示、代码跳转

这些都是 LSP 提供的功能,用到的 LSP 后端是 clangd,clangd 在安装 Xcode 后就会有,不需要再安装,因此只需在 nvim-lspconfig 激活 clangd 的默认配置即可,这里不细说。

用 nvim 打开支持的文件后 clangd 就会启动,会在文件所在目录开始往上寻找“项目根目录”,默认找到 "compile_commands.json", "compile_flags.txt", ".git" 三者之一就视为找到,如果找到的是前两者之一,clangd 则可以配置 LSP 相关功能,而通过一些方法恰好能生成 "compile_commands.json" 文件。

1. 通过 xcpretty 生成(不推荐,该工具太久未更新有 bug,生成的文件信息不全)

安装好 xcpretty 工具,在 Xcode 项目目录下运行,即可生成 compile_commands.json 文件

xcodebuild -project test.xcodeproj CODE_SIGNING_ALLOWED=NO -alltargets | xcpretty -r json-compilation-database --output compile_commands.json
  • test.xcodeproj 改为实际的项目配置文件夹
  • CODE_SIGNING_ALLOWED=NO 为不进行签名,如果有证书的话也可以签名
  • -alltargets 为编译所有 target

2. 通过脚本生成(推荐)

可以看这篇文章的详细分析,文章附带了一段脚本,内容如下

#!/usr/bin/env bash

# Global variables
readonly GXC_SCRIPT_PATH="$(test -L "${BASH_SOURCE[0]}" && readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOURCE[0]}")"
readonly GXC_SCRIPT_DIR=$(cd "$(dirname "${GXC_SCRIPT_PATH}")"; pwd)

# brew install jq
: "${JQ:=$(command -v jq)}"
: "${OUTPUT_DIR:=$PWD}"

process_fragments() {
	local -a files=()
	for file in "$@"; do
		[[ -f "$file" ]] || continue

		# Fragments generated by clang have a comma before EOF
		# If a fragment is stil invalid after removing it, it should be skipped
		if sed -e '$s/,$//' "$file" | "$JQ" . > /dev/null; then
			echo "Processing: $file" >&2
			files+=("$file")
		else
			echo "Skipping: $file" >&2
		fi
	done

	if (( ${#files[@]} == 0 )); then
		echo "No input files found!" >&2
		return
	fi

	sed -e '1s/^/[\'$'\n''/' -e '$s/,$/\'$'\n'']/' "${files[@]}" > "${OUTPUT_DIR}/compile_commands.json"
}

generate_database() {
	echo "Running build command ..." >&2
	if grep -q 'OTHER_CFLAGS' <<< "$*"; then
		echo "OTHER_CLAGS detected in build command! This is unsupported as they will be overridden." >&2
		return 1
	fi

	# xcrun xcodebuild ...
	# https://reviews.llvm.org/D66555
	
	# Note: Some tools require extra cflags to properly parse the compilation database, e.g. Infer
	# OTHER_CFLAGS="\$(inherited) -DNS_FORMAT_ARGUMENT(A)= -D_Nullable_result=_Nullable -gen-cdb-fragment-path ${OUTPUT_DIR}/CompilationDatabase"
	"$@" COMPILER_INDEX_STORE_ENABLE=NO OTHER_CFLAGS="\$(inherited) -gen-cdb-fragment-path ${OUTPUT_DIR}/CompilationDatabase"
	process_fragments "${OUTPUT_DIR}/CompilationDatabase"/*.json
}

# main
if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then
	echo "Script is being sourced: ${GXC_SCRIPT_PATH}"
else
	(( DEBUG == 0 )) || set -x
	set -euo pipefail

	if (( $# == 0 )); then
		cat <<-EOF
		Usage: $(basename "$0") <xcodebuild-command>
		Environment variables:
		JQ: Path to a jq binary
		OUTPUT_DIR: Writable directory for storing the result. Default: (\$PWD) - $PWD
		EOF

		exit 1
	fi

	generate_database "$@"
fi

# Example invocation
# generate-xcode-compilation-database.sh xcodebuild build -project TestProject.xcodeproj -target TestTarget -configuration Debug

将这段脚本保存下来,然后使用脚本 + 编译命令的方式来运行,如

generate-xcode-compilation-database.sh xcodebuild -project test.xcodeproj CODE_SIGNING_ALLOWED=NO -alltargets

注意

这两种方法执行成功后都会在执行目录下生成 compile_commands.json 文件,此时用 nvim 打开项目里的文件,就能看到 LSP 生效了。

两种方法都需要先把项目编译一遍才能获得 compile_commands.json 文件,文件里包含了参与编译的源文件的信息,根据这些信息 clangd 才能实现代码检测、补全、跳转等功能,所以我们需要尽可能的让参与到编译的源文件更多,比如可以给 xcodebuild 命令使用 -alltargets 参数。

如果需要重新生成,可以先运行 xcodebuild clean,再重复运行之前的命令。

代码格式化

这里选择很多,我用的是 formatter.nvim

给 formatter.nvim 增加以下配置即可

filetype = {
    objc = { require("formatter.filetypes.c").clangformat },
    objcpp = { require("formatter.filetypes.cpp").clangformat },
},