概要

通常,在 Mac 下开发 Cocoa 应用程序或是 iPhone 应用程序的时候使用的是 XCode ,但是习惯了 Emacs 的人也许已经不习惯这样的 IDE 了。比如本人,自从接触 Emacs 以后,基本上所有的程序,博客,工作报告等都是用它来完成的。这里,我将给大家介绍在 Emacs 下开发 iPhone 应用程序的方法,也许试过之后你也会喜欢上它的(在windows/linux下开发的用户也可以试试,一切都可以自动化的完成,参考这里这里

环境设定

设定 XCode 的外部编辑器

首先将缺省的编辑器由 XCode 更改为 Emacs。这样一来,双击 XCode 的源文件后,将用 Emacs 打开。

如下图所示,在「环境设定」->「文件类型」->「file」->「text」-> 「sourcecode」-> 「sourcecode.c」->「外部编辑器」-> 「其他」中选择「Emacs.app」。



必须选择「其他」。最初 emacs 由 Terminal 内启动。



这里,工程文件还是由 XCode 打开的。

Emacs 中管理 Objective-C 文件

利用 Emacs 开发 Objective-C 语言程序的时候,需要打开 objc-mode。

首先在 ~/.emacs.el 中设定关联 objc 语言的文件后缀名 .m 、 .mm 、.h。


(add-to-list 'auto-mode-alist '("//.mm?$" . objc-mode))
(add-to-list 'auto-mode-alist '("//.h$" . objc-mode))


但是,后缀名为 .m 的文件除了 Objective-C 以外,matlab 中也在使用,后缀名为 .h 的文件 C/C++ 中也被应用。如果只是想这样单纯的设置,应该还是会带来一些不便的。不过不要紧,在 Emacs22 以后,为了解决这个问题可以设定magic-mode-alist。它可以解析具体文件中的内容确定具体的mode。

这里,判断文件行头是否有 @implementation 、 @interface 、 @protocol ,如果有,就设定 objc-mode。


(add-to-list 'magic-mode-alist '("//(.//|/n//)*/n@implementation" . objc-mode))
(add-to-list 'magic-mode-alist '("//(.//|/n//)*/n@interface" . objc-mode))
(add-to-list 'magic-mode-alist '("//(.//|/n//)*/n@protocol" . objc-mode))


编译与执行

这里使用 xcodebuild 命令行实现命令行的编译方式,你也可以使用这里的方法,使用 gcc&Makefile 。

编译可以使用下面的命令:


xcodebuild -configuration Debug -sdk iphonesimulator3.1.2


执行可以通过 AppleScript 来实现。


tell application "Xcode" to activate
tell application "System Events"
     tell process "Xcode"
          key code 36 using {command down}
     end tell
end tell


这里直接使用了 key code 。如果你自定义了 Mac 的 key code 话,就不能正常工作了。这里使用的 key code 的意思如下:

using

意思

Unicode

菜单上的记号

command down

命令键

0x2318


control down

控制键

0x2303


option down

alt键

0x2325


shift down

shift键

0x21E7


以及


key code

esc

53

tab

48

space

49

return

36

delete

51

left arrow

123

right arrow

124

down arrow

125

up arrow

126

所以,这里的例子就是 Ctr+return 。然后将该 AppleScript 嵌入到 Emacs Lisp 中。(这里只针对 Carbon Emacs 或Cocoa Emacs 有效)


(defun xcode:buildandrun ()
 (interactive)
 (do-applescript
  (format
   (concat
    "tell application /"Xcode/" to activate /r"
    "tell application /"System Events/" /r"
    "     tell process /"Xcode/" /r"
    "          key code 36 using {command down} /r"
    "    end tell /r"
    "end tell /r"
    ))))


然后使用 M-x xcode:buildandrun 来执行。或者绑定下面的快捷键。


(add-hook 'objc-mode-hook
         (lambda ()
           (define-key objc-mode-map (kbd "C-c C-r") 'xcode:buildandrun)
         ))


查找帮助

开发程序的时候经常会用到帮助文档,类似windows下的MSDN。在 Mac 下利用命令行形式检索帮助时用 docsetutil 命令。比如下面的方法:


/Developer/usr/bin/docsetutil search /Developer/Platforms/iPhoneOS.platform/Developer/Documentation/DocSets/com.apple.adc.documentation.AppleiPhone3_1.iPhoneLibrary.docset -query 'word'


Emacs 中利用这一命令,可以使用 xcode-document-viewer.el 。运行的时候需要 emacs-w3m 。可以在 这里下载 w3m,按照下面的方法安装。


curl -O http://www.hpl.hp.com/personal/Hans_Boehm/gc/gc_source/gc.tar.gz
tar xvfz gc.tar.gz
cd gc
./configure
make
sudo make install

cd ..

tar xvfz w3m-0.5.2.tar.gz
./configure
make
sudo make install


这之后,安装 emacs-w3m 到 .emacs.d/lisp 下。


cvs -d :pserver:anonymous@cvs.namazu.org:/storage/cvsroot co emacs-w3m
cd emacs-w3m
autoconf
./configure --with-lispdir=~/.emacs.d/lisp/w3m --datarootdir=~/.emacs.d/share --with-icondir=~/.emacs.d/share/icon
make
make install
make install-icons


cd ~/.emacs.d/lispcurl -O http://www.emacswiki.org/emacs/download/anything.elcurl -O http://github.com/sakito/emacs-xcode-document-viewer/raw/master/xcode-document-viewer.el# 这里是原始版# curl -O http://github.com/imakado/emacs-xcode-document-viewer/raw/master/xcode-document-viewer.el


然后在 .emacs.el 中像下面一样设置。

;; 自动加载 emacs-w3m
(autoload 'w3m "w3m" "Interface for w3m on Emacs." t)
(require 'xcode-document-viewer)
(setq xcdoc:document-path "/Developer/Platforms/iPhoneOS.platform/Developer/Documentation/DocSets/com.apple.adc.documentation.AppleiPhone3_1.iPhoneLibrary.docset")
(setq xcdoc:open-w3m-other-buffer t)
(add-hook 'objc-mode-hook
         (lambda ()
            ;; 用 C-c w 来检索文档
            (define-key objc-mode-map (kbd "C-c w") 'xcdoc:ask-search)))


扩展设置

打开头文件

比如像打开 #import <UIKit/UIKit.h> 处的头文件时,可以使用 Emacs 中自带的 ffap(find file (or url) at point)。

快捷键是 C-x C-f ,在光标处的头文件执行它,将打开对应的头文件。


(ffap-bindings)
;; 设定搜索的路径 ffap-c-path
;; (setq ffap-c-path
;;     '("/usr/include" "/usr/local/include"))
;; 如果是新文件要确认
(setq ffap-newfile-prompt t)
;; ffap-kpathsea-expand-path 展开路径的深度
(setq ffap-kpathsea-depth 5)


另外,由 .h 文件切换到 .m 文件、或者由 .m 文件切换到对应的 .h 文件、可以使用 ff-find-other-file。

如下设置,使用 C-c o 来切换文件。


(setq ff-other-file-alist
     '(("//.mm?$" (".h"))
       ("//.cc$"  (".hh" ".h"))
       ("//.hh$"  (".cc" ".C"))

       ("//.c$"   (".h"))
       ("//.h$"   (".c" ".cc" ".C" ".CC" ".cxx" ".cpp" ".m" ".mm"))

       ("//.C$"   (".H"  ".hh" ".h"))
       ("//.H$"   (".C"  ".CC"))

       ("//.CC$"  (".HH" ".H"  ".hh" ".h"))
       ("//.HH$"  (".CC"))

       ("//.cxx$" (".hh" ".h"))
       ("//.cpp$" (".hpp" ".hh" ".h"))

       ("//.hpp$" (".cpp" ".c"))))
(add-hook 'objc-mode-hook
         (lambda ()
           (define-key c-mode-base-map (kbd "C-c o") 'ff-find-other-file)
         ))


补全

在 Emacs 中也能完成 Objective-C 的补全功能。设立,我们使用 auto-complete 、 company-mode 、 ac-company。

安装


cd ~/.emacs.d
mkdir lisp
cd lisp
curl -O http://github.com/m2ym/auto-complete/raw/master/auto-complete.el
curl -O http://github.com/m2ym/auto-complete/raw/master/auto-complete-config.el
curl -O http://github.com/m2ym/auto-complete/raw/master/popup.el
curl -O http://github.com/m2ym/auto-complete/raw/master/fuzzy.el
curl -O http://nschum.de/src/emacs/company-mode/company-0.4.3.tar.bz2
curl -O http://github.com/buzztaiki/auto-complete/raw/master/ac-company.el
tar xvfj company-0.4.3.tar.bz2


设置

在 .emacs.el 中添加下面的设置:


;; load-path 路径
(let ((default-directory (expand-file-name "~/.emacs.d/lisp")))
 (add-to-list 'load-path default-directory)
 (if (fboundp 'normal-top-level-add-subdirs-to-load-path)
     (normal-top-level-add-subdirs-to-load-path)))

;; 加载
(require 'auto-complete)
(require 'auto-complete-config)
(require 'ac-company)

(global-auto-complete-mode t)
;; ac-company 中设置 company-xcode 有效
(ac-company-define-source ac-source-company-xcode company-xcode)
;; 设定 objc-mode 中补全 ac-mode
(setq ac-modes (append ac-modes '(objc-mode)))
;; hook
(add-hook 'objc-mode-hook
         (lambda ()
           (define-key objc-mode-map (kbd "/t") 'ac-complete)
           ;; 使用 XCode 的补全功能有效
           (push 'ac-source-company-xcode ac-sources)
           ;; C++ 关键词补全
           (push 'ac-source-c++-keywords ac-sources)
         ))
;; 补全窗口中的热键
(define-key ac-completing-map (kbd "C-n") 'ac-next)
(define-key ac-completing-map (kbd "C-p") 'ac-previous)
(define-key ac-completing-map (kbd "M-/") 'ac-stop)
;; 是否自动启动补全功能
(setq ac-auto-start nil)
;; 启动热键
(ac-set-trigger-key "TAB")
;; 候補的最大件数(缺省 10件)
(setq ac-candidate-max 20)


如果不能很好的完成补全,先用 XCode 编译一次源代码,然后再试应该没有什么问题了。因为上记补全的方法实际上是使用了 XCode 的 xcodeindex 命令,需要动态地收集补全的信息。

etags

如果你不喜欢这种方式,还可以试试 etags(或者 gtags,这里只介绍 etags,有兴趣的朋友可以自己试试 gtags )。它主要是利用了源代码文件(类名,函数名等)来建立索引(tag)。

首先,使用 etags 命令生成tag文件。以下的例子生成 tag 到 ~/.emacs.d/share/tags 下。


cd ~/.emacs.d
mkdir -p share/tags
cd share/tags
find /Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS${VER}.sdk/System/Library/Frameworks -name "*.h" | xargs etags -f objc.TAGS -l objc


生成的文件名为 objc.TAGS ,内部只是类的名称。如果要得到比较详细的信息(函数名等)使用下面的shell脚本。


#!/bin/sh
s="/t "
S="[$s]*"
w="_a-zA-Z0-9"
CN="[A-Z][$w]*"
NM="[$w][$w]*"

SDK="/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS${VER}.sdk/System/Library/Frameworks"

find $SDK -name "*.h" | xargs etags -a --declarations -r "/$S[-+]$S(/($S$NM/)/{1,3/}$S/**$S)?$S/($NM/)$S[:;]//2/" -f frm.tags
sed "/^@class/d" frm.tags > objc.TAGS


tag 文件比较大,这里我们只是作为 objc-mode 的补全候补来使用,这里使用到了 etags-table.el。

安装


cd ~/.emacs.d/lisp
curl -O http://bitbucket.org/sakito/dot.emacs.d/raw/tip/local-lisp/etags-table.el


设置

在 .emacs.el 文件中添加下面的设置:


;; etags-table 有效
(require 'etags-table)
(add-to-list  'etags-table-alist
              '("//.[mh]$" "~/.emacs.d/share/tags/objc.TAGS"))
;; auto-complete 中待确认的 etags 参数
;; 3文字以上时,补全功能有效
(defvar ac-source-etags
  '((candidates . (lambda ()
         (all-completions ac-target (tags-completion-table))))
    (candidate-face . ac-candidate-face)
    (selection-face . ac-selection-face)
    (requires . 3))
  "etags source")
(add-hook 'objc-mode-hook
          (lambda ()
            (push 'ac-source-etags ac-sources)))


另外,使用 etags 除了补全功能以外,还可以在代码间跳转。使用“M+.”跳到光标处源代码位置(比如函数定义处),使用“Alt+*”还可以跳回来。

Text macros(模板)

XCode 中有一个名为「Text macros」的功能,使用它可以自动生成模板代码,提高了开发的效率,Emacs 中 YASnippet 就可以实现同样的功能。

安装


cd ~/.emacs.d/lisp
curl -O http://yasnippet.googlecode.com/files/yasnippet-0.6.1c.tar.bz2
tar xvfj yasnippet-0.6.1c.tar.bz2
cd yasnippet-0.6.1c


设置

在 .emacs.el 文件中添加下面的设置:


(let ((default-directory (expand-file-name "~/.emacs.d/lisp")))
 (add-to-list 'load-path default-directory)
 (if (fboundp 'normal-top-level-add-subdirs-to-load-path)
     (normal-top-level-add-subdirs-to-load-path)))

(require 'yasnippet)
;; 设置snippet的位置
(setq yas/root-directory "~/.emacs.d/lisp/yasnippet-0.6.1c/snippets")
;; 不要菜单
(setq yas/use-menu nil)
;; 初始化
(yas/initialize)
(yas/load-directory yas/root-directory)


这样一来,在 objc-mode 中按下Tab键、就可以启动 YASnippet 了。你可以输入 for 按后点击tab试试。

但是 YASnippet 中供 objc-mode 使用的 snippets 并不是很多、利用 XCode 的 TextMacro 可以解决这个问题。


XCode 的 TextMacros 位于下面的位置,因为是文本文件,你可以用 [[Emacs]] 打开来查看。 /Developer/Applications/Xcode.app/Contents/PlugIns/TextMacros.xctxtmacro/ Contents/Resources/ObjectiveC.xctxtmacro


首先添加新的目录用来保存 snippets。这里我们创建 ~/.emacs.d/etc/snippets 目录,然后在 .emacs.el 文件中设置:


;; 设置复数的 snippets 路径
(setq yas/root-directory '("~/.emacs.d/lisp/yasnippet-0.6.1c/snippets"
                           "~/.emacs.d/etc/snippets"))
(mapc 'yas/load-directory yas/root-directory)


在 ~/.emacs.d/etc/snippets 目录下创建 objc-mode 子目录、在其下创建后缀名为 .yasnippet 的模板文件。比如像下面 try.yasnippet 的文件。


# -*- mode: snippet -*-
#name : @try { ... } @catch { ... } @finally { ... }
# --
@try {
    $1
}
@catch (NSException * e) {
    $2
}
@finally {
    $3
}$0


你可以参考 XCode 中的 TextMacros 实现所需的模板。

自动插入匹配的括号

Emacs 中标准的括号自动插入功能如下所示:


(add-hook 'c-mode-common-hook
         '(lambda()
            ;; 插入对称的括号
            (make-variable-buffer-local 'skeleton-pair)
            (make-variable-buffer-local 'skeleton-pair-on-word)
            (setq skeleton-pair-on-word t)
            (setq skeleton-pair t)
            (make-variable-buffer-local 'skeleton-pair-alist)
            (local-set-key (kbd "(") 'skeleton-pair-insert-maybe)
            (local-set-key (kbd "[") 'skeleton-pair-insert-maybe)
            (local-set-key (kbd "{") 'skeleton-pair-insert-maybe)
            (local-set-key (kbd "`") 'skeleton-pair-insert-maybe)
            (local-set-key (kbd "/"") 'skeleton-pair-insert-maybe)
            ))


你也可以使用 smartchr.el ,也许能更方便一些。


(defun ik:insert-eol (s)
  (interactive)
  (lexical-let ((s s))
    (smartchr-make-struct
     :insert-fn (lambda ()
                  (save-excursion
                    (goto-char (point-at-eol))
                    (when (not (string= (char-to-string (preceding-char)) s))
                      (insert s))))
     :cleanup-fn (lambda ()
                   (save-excursion
                     (goto-char (point-at-eol))
                     (delete-backward-char (length s)))))))

(defun ik:insert-semicolon-eol ()
  (ik:insert-eol ";"))

(defun smartchr-custom-keybindings ()
  (local-set-key (kbd "=") (smartchr '(" = " " == "  "=")))
  (local-set-key (kbd "(") (smartchr '("(`!!')" "(")))
  (local-set-key (kbd "[") (smartchr '("[`!!']" "[ [`!!'] ]" "[")))
  (local-set-key (kbd "{") (smartchr '("{/n`!!'/n}" "{`!!'}" "{")))
  (local-set-key (kbd "`") (smartchr '("/``!!''" "/`")))
  (local-set-key (kbd "/"") (smartchr '("/"`!!'/"" "/"")))
  (local-set-key (kbd ">") (smartchr '(">" " => " " => '`!!''" " => /"`!!'/"")))
  (lobal-set-key (kbd "F") (smartchr '("F" "$" "$_" "$_->" "@$")))
  (lobal-set-key (kbd "j") (smartchr '("j" ik:insert-semicolon-eol)))
  )

(defun smartchr-custom-keybindings-objc ()
  (local-set-key (kbd "@") (smartchr '("@/"`!!'/"" "@")))
  )

(add-hook 'c-mode-common-hook 'smartchr-custom-keybindings)
(add-hook 'objc-mode-hook 'smartchr-custom-keybindings-objc)


当你按下一位「==」键时,自动输出「 = 」,前后自动添加空白。当再一次输入「=」时,得到的是「 == 」。另外也可以匹配地输入带改行的文字,自定义各种特定的输入等。(比如这里,2回按下“j”键后,自动在改行末尾添加“;”)。

缩进

将下面的设定添加到 .emacs.el 中,使用tab的距离为4个空白位。


(add-hook 'c-mode-common-hook
         '(lambda()
             (c-set-style "cc-mode")))

(setq-default indent-tabs-mode nil)
(setq-default tab-width 4)


选择矩形区域

使用 cua-mode 可以方便地实现代码中的矩形选择。将下面的代码添加到 .emacs.el 中,使用 C-RET 可以进入矩形选择模式。


(setq cua-enable-cua-keys nil)(cua-mode t)


语法检查

可以使用 flymake 来完成语法检查。在 .emacs.el 中添加下面的设置:


(require 'flymake)
(defvar xcode:gccver "4.0")
(defvar xcode:sdkver "3.1.2")
(defvar xcode:sdkpath "/Developer/Platforms/iPhoneSimulator.platform/Developer")
(defvar xcode:sdk (concat xcode:sdkpath "/SDKs/iPhoneSimulator" xcode:sdkver ".sdk"))
(defvar flymake-objc-compiler (concat xcode:sdkpath "/usr/bin/gcc-" xcode:gccver))
(defvar flymake-objc-compile-default-options (list "-Wall" "-Wextra" "-fsyntax-only" "-ObjC" "-std=c99" "-isysroot" xcode:sdk))
(defvar flymake-last-position nil)
(defvar flymake-objc-compile-options '("-I."))
(defun flymake-objc-init ()
  (let* ((temp-file (flymake-init-create-temp-buffer-copy
                    'flymake-create-temp-inplace))
         (local-file (file-relative-name
                     temp-file
                     (file-name-directory buffer-file-name))))
    (list flymake-objc-compiler (append flymake-objc-compile-default-options flymake-objc-compile-options (list local-file)))))

(add-hook 'objc-mode-hook
         (lambda ()
           (push '("//.m$" flymake-objc-init) flymake-allowed-file-name-masks)
           (push '("//.h$" flymake-objc-init) flymake-allowed-file-name-masks)
           (if (and (not (null buffer-file-name)) (file-writable-p buffer-file-name))
               (flymake-mode t))
         ))


如果检查到有语法错误,有错误的代码行自动显示到 minibuffer 中,如下设置。


(defun flymake-display-err-minibuffer ()
  "改行有 error 或 warinig 显示在 minibuffer"
  (interactive)
  (let* ((line-no (flymake-current-line-no))
         (line-err-info-list (nth 0 (flymake-find-err-info flymake-err-info line-no)))
         (count (length line-err-info-list)))
    (while (> count 0)
      (when line-err-info-list
        (let* ((file (flymake-ler-file (nth (1- count) line-err-info-list)))
               (full-file (flymake-ler-full-file (nth (1- count) line-err-info-list)))
               (text (flymake-ler-text (nth (1- count) line-err-info-list)))
               (line (flymake-ler-line (nth (1- count) line-err-info-list))))
          (message "[%s] %s" line text)))
      (setq count (1- count)))))

(defadvice flymake-goto-next-error (after display-message activate compile)
  "下一个错误"
  (flymake-display-err-minibuffer))

(defadvice flymake-goto-prev-error (after display-message activate compile)
  "前一个错误"
  (flymake-display-err-minibuffer))

(defadvice flymake-mode (before post-command-stuff activate compile)
  "为了将问题行自动显示到 minibuffer 中,添加 post command hook "
  (set (make-local-variable 'post-command-hook)
       (add-hook 'post-command-hook 'flymake-display-err-minibuffer)))

;; post-command-hook 与 anything.el 有冲突时使用
(define-key global-map (kbd "C-c d") 'flymake-display-err-minibuffer)


你可以把 flymake-goto-next-error 与 flymake-goto-prev-error 分配到自己喜欢的快捷键上。另外像下面给错误附上颜色,便于区分。


(set-face-background 'flymake-errline "red")
(set-face-background 'flymake-warnline "yellow")


启动优化

用过 Emacs 的朋友也许都知道, 随着功能模块的增多,Emacs 的启动速度是越来越慢。这里我们介绍一种加速的方法 — 将 Emacs Lisp 编译为2进制文件。以加快其启动速度。


emacs -batch -f batch-byte-compile *.el
# Emacs.app
/Applications/Emacs.app/Contents/MacOS/Emacs -batch -f batch-byte-compile *.el