Master Emacs in 21 Days

Table of Contents

Spacemacs-rocks.svg?branch=master

Spacemacs Rocks 第二季

Artwork.png

GitHub Spacemacs Rocks 作者 zilongshanren 编者 li-xinyang, lsytj0413

在这一季中我们计划用 21 天学习 Emacs 以及 Spacemacs 的使用。从基础安装开始到可以 运用到工作生产环节中。为了确保学习质量,请务必完成和理解计划中的每一项学习任务。

第一天:准备开始

视频地址如下

说在最前面

如果你第一次听说 Emacs 你可以在性感的 Emacs(Emacs is Sexy)一文中找到使用它的理 由!我相信你一定会被它的强大所吸引。

如果你还没有安装 Emacs 可以在 Mac OS X 安装链接以及 Window 安装链接找到它。本文 我们使用的为 Emacs 25.0.x Pretest 版本,一些配置在 Emacs 24 中可能并不适用。

在开始本教程之前请务必先完成 Emacs 提供的官方教程(完成时间大约 30 分钟),它可 以通过使用 C-h t (同时按住 Ctrl 与 h 键,接着按 t 键 t 在这里代表 tutorial) 在 Emacs 中直接将其打开。

Emacs Lisp 是 Emacs 所用的编程语言,你可以在阅读这篇教程(Learn X in Y Minutes) 后很快地了解它的基础用法。

基础操作

因为 Control 键在 Emacs 中极为常用,所以为了更方便地使用 Emacs 的键位修改,需要 对其做出修改。建议将大写锁定键(Caps Lock)替换为 Control 键。这里是一个可以参考 的把大写锁定键替换为 Control 键的教程:移动 Control 键 (Moving The Ctrl Key)。如 果这个的教程不适用于你的操作系统,请自行搜索其它的修改方式。

常见符号所代表的意义如下

  • M(eta),在 Mac 下为 Option 键
  • s(uper),在 Mac 环境下为左 Command 键
  • S(Shift)
  • C(trl)

光标的移动是编辑器中最常用的操作所以必须熟知。

  • C-f 为前移一个字符, f 代表 forward。
  • C-b 为后移一个字符, b 代表 backward。
  • C-p 为上移至前一行, p 代表 previous。
  • C-n 为上移至下一行, n 代表 next。
  • C-a 为移至行首, a 代表 ahead。
  • C-e 为移至行尾, e 代表 end。

常用的文件操作快捷键组合也必须熟记。

  • C-x C-f 为打开目标文件, f 代表 find/file
  • C-x C-s 为保存当前缓冲区(Buffer), s 代表 save

C-x 是 Emacs 的快捷键中常用的前缀命令。这些前缀命令常常代表了一系列有关联的指 令,十分重要,请特别牢记。其它常见的还有 C-c, C-h 。打断组合键为 C-g ,它 用于终端取消之前的指令。快捷键就是用预先绑定好的方式告诉 Emacs 去执行指定的命令。 (之后会介绍到更多有关绑定的内容)

内置功能

Emacs 功能强大,但是部分功能默认情况下并未开启。下面就有几个例子,

编辑器内显示行号可使用 M-x linum-mode 来开启。

获取帮助

Emacs 是一个富文档编辑器(Self document, extensible editor)而下面的三种方法在学 习 Emacs 的过程中也非常重要。他们分别是,

  • C-h k 寻找快捷键的帮助信息
  • C-h v 寻找变量的帮助信息
  • C-h f 寻找函数的帮助信息

学习基础 Elisp

请务必完成这篇教程(Learn X in Y Minutes)来了解 Elisp 的使用(阅读时间大约 15 分钟),你也可以在这里找到它的中文版。Emacs Lisp 为一个函数式的语言,所以它全部 功能都是由函数来实现的。

下面为一些简单的例子

;; 2 + 2
(+ 2 2)

;; 2 + 3 * 4
(+ 2 (* 3 4))

;; 定义变量
(setq name "username")
(message name) ; -> "username"

;; 定义函数
(defun func ()
  (message "Hello, %s" name))

;; 执行函数
(func) ; -> Hello, username

;; 设置快捷键
(global-set-key (kbd "<f1>") 'func)

;; 使函数可直接被调用可添加 (interactive)
(defun func ()
  (interactive)
  (message "Hello, %s" name))

开始 Hacking!

Emacs 的配置文件默认保存在 ~/.emacs.d/init.el 文件中。(如果其不存在可自行创建, 配置文件也可保存在 ~/.emacs 文件中,他们之间的区别我们会在后面做讨论)

注意: 如果希望把配置放在 ~/.emacs.d/init.el 文件中,那么需要手工删除 ~/.emacs 文件。

在开始配置之前让我们先来区别 Emacs 中 Major Mode 与 Minor Mode 的区别。Major Mode 通常是定义对于一种文件类型编辑的核心规则,例如语法高亮、缩进、快捷键绑定等。 而 Minor Mode 是除去 Major Mode 所提供的核心功能以外的额外编辑功能(辅助功能)。 例如在下面的配置文件中 tool-bar-modelinum-mode 等均为 Minor Mode*。

简单来说就是,一种文件类型同时只能存在一种 Major Mode 但是它可以同时激活一种或多 种 Minor Mode。如果你希望知道当前的模式信息,可以使用 C-h m 来显示当前所有开启 的全部 Minor Mode 的信息。

简单的编辑器自定义

下面是一些简单的编辑器配置信息,你需要做的就是将其写入你的配置文件中 ( ~/.emacs.d/init.el )即可。

;; 关闭工具栏,tool-bar-mode 即为一个 Minor Mode
(tool-bar-mode -1)

;; 关闭文件滑动控件
(scroll-bar-mode -1)

;; 显示行号
(global-linum-mode 1)

;; 更改光标的样式(不能生效,解决方案见第二集)
(setq cursor-type 'bar)

;; 关闭启动帮助画面
(setq inhibit-splash-screen 1)

;; 关闭缩进 (第二天中被去除)
;; (electric-indent-mode -1)

;; 更改显示字体大小 16pt
;; http://stackoverflow.com/questions/294664/how-to-set-the-font-size-in-emacs
(set-face-attribute 'default nil :height 160)

;; 快速打开配置文件
(defun open-init-file()
  (interactive)
  (find-file "~/.emacs.d/init.el"))

;; 这一行代码,将函数 open-init-file 绑定到 <f2> 键上
(global-set-key (kbd "<f2>") 'open-init-file)

在每次编辑配置文件后,刚刚做的修改并不会立刻生效。这时你需要重启编辑器或者重新加 载配置文件。重新加载配置文件你需要在当前配置文件中使用 M-x load-file 双击两次 回车确认默认文件名,或者使用 M-x eval-buffer 去执行当前缓冲区的所有 Lisp 命令。 你也可以使用 C-x C-e 来执行某一行的 Lisp 代码。这些可使刚刚修改的配置文件生效。 当然你也可以将这些函数绑定为快捷键。

插件管理

使用默认的插件管理系统(可在菜单栏 Options > Manage Emacs Packages 中找到)安 装 Company 插件,他是一个用于代码补全的插件。它的名字代表补全一切的意思( Comp lete Any thing)。因为默认的插件管理系统提供的插件十分有限,所以我们会在之后的 几天中继续将其强化。

使用的下面的配置将 Company-mode 在全局模式下激活

; 开启全局 Company 补全
(global-company-mode 1)

Org-mode

简单的 Org-mode 使用,它可以列出提纲,并方便地使用 tab 键来对其进行展开与关闭。 C-c C-t 可以将一个条目转换成一条待办事件。

* 为一级标题
** 为二级标题
*** 为三级标题并以此类推

第二天:高级自定义

视频地址如下

说在最前面

如果你想深入学习 Emacs Lisp 可以阅读 GNU 提供的 An Introduction to Programming in Emacs Lisp 。(也可以 M-x info 然后选择 Emacs Lisp Intro)

我们先解决前一天中遇到的一些问题。首先是在对象是一个缓冲区局部变量(Buffer-local variable)的时候,比如这里的 cursor-type ,我们需要区分 setqsetq-defaultsetq 设置当前缓冲区(Buffer)中的变量值, setq-default 设 置的为全局的变量的值(具体内容可以在 StackOverflow 找到)。下面是一个例子,用于 设置光标样式的方法。

(setq-default cursor-type 'bar)

今天我们需要将第一天关闭的自动缩进 (electric-indent-mode) 从配置文件中去除,它 是 Emacs 24.4 中加入的新特性,你可以在这篇文章中找到更多关于它的内容。我们之前关 闭它是因为,它存在不理想的缩进效果(在 Emacs Lisp 中用分号做注释时 fancy-comment 会造成很远的缩进,其实解决方法是使用 Emacs Lisp 推荐的两个分号而 不是一个 ;; ,这样就可以避免这个问题。于是我们也就将其从配置文件中删除)

因为通常我们的配置文件以及项目文件均使用版本控制系统,所以自动生成的备份文件就显 得有些多余。我们还可以禁止 Emacs 自动生成备份文件,例如 init.el~ 。( ~ 为后 缀的文件为自动生成的备份文件)我们可以使用下面的方法将其关闭。

(setq make-backup-files nil)

关于分屏的使用,如果你已经读过 Emacs 自带的教程,现在你应该已经掌握了基本的分屏 操作方法了。关于分屏的更多内容你可以在这里找到。

  • C-x 1 仅保留当前窗口
  • C-x 2 将当前窗口分到上边
  • C-x 3 将当前窗口分到右边

使用下面的配置来加入最近打开过文件的选项让我们更快捷的在图形界面的菜单中打开最近 编辑过的文件。

(require 'recentf)
(recentf-mode 1)
(setq recentf-max-menu-item 10)

;; 这个快捷键绑定可以用之后的插件 counsel 代替
;; (global-set-key (kbd "C-x C-r") 'recentf-open-files)

require 的意思为从文件中加载特性,你可以在杀哥的网站读到关于 Emacs Lisp 库系统 的更多内容,文章在这里

使用下面的配置文件将删除功能配置成与其他图形界面的编辑器相同,即当你选中一段文字 之后输入一个字符会替换掉你选中部分的文字。

(delete-selection-mode 1)

下面的这些函数可以让你找到不同函数,变量以及快捷键所定义的文件位置。因为非常常用 所以我们建议将其设置为与查找文档类似的快捷键(如下所示),

  • find-functionC-h C-f
  • find-variableC-h C-v
  • find-function-on-keyC-h C-k

在我们进入下一个部分之间让我们来看看使用 ~/.emacs.d/init.el~/.emacs 的区 别(更多关于他们区别的讨论可在这里找到)。简单来说请使用前者,因为它有下面的两个 优点,

  • 它可以更好将所有 Emacs 相关的文件整合在一个目录内(干净的 HOME ,网盘备份等优点)
  • 更好的版本控制

Emacs 也很美

配置插件源

在进行美化之前我们需要配置插件的源(默认的源非常有限),最常使用的是 MELPA (Milkypostman's Emacs Lisp Package Archive)。它有非常多的插件(3000 多个插件)。 一个插件下载的次数多并不能说明它非常有用,也许这个插件是其他的插件的依赖。在这里 你可以找到其安装使用方法。添加源后,我们就可以使用 M-x package-list-packages 来查看所有 MELPA 上的插件了。在表单中可以使用 i 来标记安装 d 来标记删除, U 来更新,并用 x 来确认。你也可以使用 u 来撤销标记操作。

你可以直接将下面的代码复制到你的配置文件顶端,从而直接使用 Melpa 作为插件的源。 你可以将你需要的插件名字写在 my/packages 中,Emacs 在启动时会自动下载未被安装 的插件。

 (when (>= emacs-major-version 24)
     (require 'package)
     (package-initialize)
     (setq package-archives '(("gnu"   . "http://elpa.emacs-china.org/gnu/")
		      ("melpa" . "http://elpa.emacs-china.org/melpa/"))))

;; 注意 elpa.emacs-china.org 是 Emacs China 中文社区在国内搭建的一个 ELPA 镜像

 ;; cl - Common Lisp Extension
 (require 'cl)

 ;; Add Packages
 (defvar my/packages '(
		;; --- Auto-completion ---
		company
		;; --- Better Editor ---
		hungry-delete
		swiper
		counsel
		smartparens
		;; --- Major Mode ---
		js2-mode
		;; --- Minor Mode ---
		nodejs-repl
		exec-path-from-shell
		;; --- Themes ---
		monokai-theme
		;; solarized-theme
		) "Default packages")

 (setq package-selected-packages my/packages)

 (defun my/packages-installed-p ()
     (loop for pkg in my/packages
	   when (not (package-installed-p pkg)) do (return nil)
	   finally (return t)))

 (unless (my/packages-installed-p)
     (message "%s" "Refreshing package database...")
     (package-refresh-contents)
     (dolist (pkg my/packages)
       (when (not (package-installed-p pkg))
	 (package-install pkg))))

 ;; Find Executable Path on OS X
 (when (memq window-system '(mac ns))
   (exec-path-from-shell-initialize))

关于上面这段配置代码有几个知识点,首先就是这段配置文件中用到了 loop for ... in ,它来自 cl 即 Common Lisp 扩展。 for , in, collect 均为 cl-loop 中的 保留关键字。下面是一些简单的 cl-loop 的使用示例:

;; 遍历每一个缓冲区(Buffer)
(cl-loop for buf in (buffer-list)
	 collect (buffer-file-name buf))

;; 寻找 729 的平方根(设置最大为 100 为了防止无限循环)
(cl-loop for x from 1 to 100
	 for y = (* x x)
	 until (>= y 729)
	 finally return (list x (= y 729)))

你可以在这里找到更多关于循环的使用说明。

其次就是它使用到了 quote, 它其实就是我们之前常常见到的 ' (单引号)的完全体。 因为它在 Lisp 中十分常用,所以就提供了简写的方法。

;; 下面两行的效果完全相同的
(quote foo)
'foo

quote 的意思是不要执行后面的内容,返回它原本的内容(具体请参考下面的例子)

(print '(+ 1 1)) ;; -> (+ 1 1)
(print (+ 1 1))  ;; -> 2

更多关于 quote 的内容可以在这里找到,或者在这里找到 StackOverflow 上对于它的讨论。

这样我们就可以区分下面三行代码的区别,

;; 第一种
(setq package-selected-packages my/packages)
;; 第二种
(setq package-selected-packages 'my/packages)
;; 第三种
(setq package-selected-packages (quote my/packages))

第一种设置是在缓冲区中设置一个名为 package-selected-packages 的变量,将其的值 设定为 my/packages 变量的值。第二种和第三种其实是完全相同的,将一个名为 package-selected-packages 的变量设置为 my/packages

我们可以用下面代码将 Emacs 设置为开启默认全屏,

(setq initial-frame-alist (quote ((fullscreen . maximized))))

我们也可以启用自动括号匹配(Highlight Matching Parenthesis),随后会介绍插件来增 强这个匹配的功能。你可以在这里读到关于钩子的更多信息。

(add-hook 'emacs-lisp-mode-hook 'show-paren-mode)

高亮当前行,当文本内容很多时可以很容易找到光标的位置。

(global-hl-line-mode 1)

安装主题

(add-to-list 'my/packages 'monokai-theme)

然后使用下面的配置使其每次打开编辑器时加载主题,

(load-theme 'monokai 1)

推荐插件

使用 M-x customize-group 后选择对应的插件名称,可以进入可视化选项区对指定的插 件做自定义设置。当选择 Save for future session 后,刚刚做的设计就会被保存在你的 配置文件( init.el )中。关于各个插件的安装与使用方法通常都可以在其官方页面找 到(GitHub Pages 或者是项目仓库中的 README 文件)。我们强烈建议大家在安装这些插 件后阅读使用方法来更好的将它们使用到你的日常工作当中使效率最大化。

JavaScript IDE

Emacs 提供的默认 JavaScript Major Mode 并不是非常好用。所以我们可以将默认的模式 替换成 js2-mode 一个比默认模式好用的 Major Mode。我们可以通过 MELPA 来下载它,然 后用下面的代码将其启用。

(setq auto-mode-alist
      (append
       '(("\\.js\\'" . js2-mode))
       auto-mode-alist))

你可以在这里(How Emacs Chooses a Major Mode)找到 Emacs 是如何选择何时该选用何 种 Major Mode 的方法。

在这里我们需要知道 auto-mode-alist 的作用,这个变量是一个 AssociationList,它 使用正则表达式(REGEXP)的规则来匹配不同类型文件应使用的 Major Mode。 下面是几个 正则表达式匹配的例子,

(("\\`/tmp/fol/" . text-mode)
 ("\\.texinfo\\'" . texinfo-mode)
 ("\\.texi\\'" . texinfo-mode)
 ("\\.el\\'" . emacs-lisp-mode)
 ("\\.c\\'" . c-mode)
 ("\\.h\\'" . c-mode)
 …)

下面是如何添加新的模式与对应文件类型的例子(与我们配置 js2-mode 时相似的例子),

(setq auto-mode-alist
  (append
   ;; File name (within directory) starts with a dot.
   '(("/\\.[^/]*\\'" . fundamental-mode)
     ;; File name has no dot.
     ("/[^\\./]*\\'" . fundamental-mode)
     ;; File name ends in ‘.C’.
     ("\\.C\\'" . c++-mode))
   auto-mode-alist))

js2-mode 模式中会提供

  • 语法高亮
  • 语法检查器(Linter)

执行缓冲区的代码可以使用 nodejs-repl 插件,它需要你的机器上已经安装了 NodeJS。 然而在 Mac OS X 上可能会出现找不到 NodeJS 可执行文件的问题,要解决这个问题你需要 安装另外一个 exec-path-from-shell 的插件并将其启用。

(when (memq window-system '(mac ns))
  (exec-path-from-shell-initialize))

有了 nodejs-repl 我们就可以方便的测试和开发我们的 JavaScript 代码了(你可以在 这里找到更多关于它的使用方法)。

Org-mode 进阶

在 Org-mode 中你可以直接开启新的缓冲区(Buffer)直接用相应的 Major Mode 来编辑代 码块内的内容。在代码块中使用 C-c ' 会直接打开对应模式的缓冲区(不仅限于 Lisp)。 这样就使在 Org-mode 中编辑代码变的十分方便快捷。

使用 <s 然后 Tab 可以直接插入代码块的代码片段(Snippet),更多类似的代码片段 (Org-mode Easy Templates)可以在这里找到。

#+BEGIN_SRC emacs-lisp
  ;; Your code goes here
  ;; 你的代码写在这里
#+END_SRC

添加 Org-mode 文本内语法高亮

(require 'org)
(setq org-src-fontify-natively t)

在 Org-mode 中重置有序列表序号可以直接使用 M-<RET> 。

Agenda 的使用

;; 设置默认 Org Agenda 文件目录
(setq org-agenda-files '("~/org"))

;; 设置 org-agenda 打开快捷键
(global-set-key (kbd "C-c a") 'org-agenda)

你只需将你的 *.org 文件放入上面所指定的文件夹中就可以开始使用 Agenda 模式了。

  • C-c C-s 选择想要开始的时间
  • C-c C-d 选择想要结束的时间
  • C-c a 可以打开 Agenda 模式菜单并选择不同的可视方式( r

第三天:配置文件模块化(上)

视频地址如下

多文件存储配置文件(上)

将不同的配置代码放置到不同的文件中,使其模块化,这让我们的后续维护变得更加简单。 下面是我们现在的 ~/.emacs.d/ 目录中的样子,

├── auto-save-list # 自动生成的保存数据
├── elpa           # 下载的插件目录
├── init.el        # 我们的配置文件
└── recentf        # 最近访问的文件列表

通常我们只保存配置文件和对其进行版本控制,其他的插件均为在第一次使用编辑器时再通 过网络重新下载,当然你也可以选择将全部配置文件进行版本控制来保证自己时刻拥有最稳 定的生产环境。

Elisp 中并没有命名空间(Namespace),换句话说就是所有的变量均为全局变量,所以其 命名方法就变的非常重要。下面是一个简单的命名规则,

#自定义变量可以使用自己的名字作为命名方式(可以是变量名或者函数名)
my/XXXX

#模式命名规则
ModeName-mode

#模式内的变量则可以使用
ModeName-VariableName

遵守上面的命名规则可以最大程度的减少命名冲突发生的可能性。

现在我们想将原本混合在一起的配置文件分为下面的几个模块(每一个模块为一个独立的配 置文件并将其保存在指定的子目录中),它们分别是

init-packages.el        # 插件管理
init-ui.el              # 视觉层配置
init-better-defaults.el # 增强内置功能
init-keybindings.el     # 快捷键绑定
init-org.el             # Org 模式相关的全部设定
custome.el              # 存放使用编辑器接口产生的配置信息

下面为将配置文件进行模块化后的目录结构,

├── init.el
└── lisp
    ├── custom.el
    ├── init-better-defaults.el
    ├── init-helper.el
    ├── init-keybindings.el
    ├── init-packages.el
    ├── init-ui.el
    └── init-org.el

使用模块化配置就可以让我们在之后的配置中迅速的定位与更改配置内容,让整个过程变得 更有条理也更加高效。

和之前一样 init.el 是配置文件的入口,现在它便成为了所有模块配置文件的入口,所 以要使用这些模块时,我们需要在其中引用需要加载的模块。下面以 init-packages.el (此配置为添加插件的模块) 为例,详细说明如何模块化以及应用的方法。

下面是在模块化配置之前,我们所使用的配置文件 ~/.emacs.d/init.el 的样子,我们将 所有的配置代码都放置在了同一个文件中(如下所示)

下面为 ~/.emacs.d/init.el 文件的内容

;;  __        __             __   ___
;; |__)  /\  /  ` |__/  /\  / _` |__
;; |    /~~\ \__, |  \ /~~\ \__> |___
;;                      __   ___        ___      ___
;; |\/|  /\  |\ |  /\  / _` |__   |\/| |__  |\ |  |
;; |  | /~~\ | \| /~~\ \__> |___  |  | |___ | \|  |
(when (>= emacs-major-version 24)
    (require 'package)
    (package-initialize)
    (setq package-archives '(("gnu"   . "http://elpa.emacs-china.org/gnu/")
			 ("melpa" . "http://elpa.emacs-china.org/melpa/"))))

;; cl - Common Lisp Extension
(require 'cl)

;; Add Packages
(defvar my/packages '(
			   ;; --- Auto-completion ---
			   company
			   ;; --- Better Editor ---
			   smooth-scrolling
			   hungry-delete
			   swiper
			   counsel
			   smartparens
			   ;; --- Major Mode ---
			   js2-mode
			   markdown-mode
			   ;; --- Minor Mode ---
			   ;; Quick Note Taking
			   deft
			   ;; JavaScript REPL
			   nodejs-repl
			   ;; Find OS X Executable Helper Package
;; ...

之前为了更好的区分不同的区域我使用的方法是使用 ASCII Art 然后再以关键字来做搜索 跳转,但是这样再编辑工程中依旧十分缓慢和麻烦。于是我们现在要将配置文件全部模块化, 把不同部分的配置代码放置在不同的配置文件中,并在入口文件( ~/.emacs.d/init.el )中依次引用不用的模块。

下面为 ~/.emacs.d/lisp/init-packages.el 模块中的代码

;;  __        __             __   ___
;; |__)  /\  /  ` |__/  /\  / _` |__
;; |    /~~\ \__, |  \ /~~\ \__> |___
;;                      __   ___        ___      ___
;; |\/|  /\  |\ |  /\  / _` |__   |\/| |__  |\ |  |
;; |  | /~~\ | \| /~~\ \__> |___  |  | |___ | \|  |
(when (>= emacs-major-version 24)
    (require 'package)
    (package-initialize)
    (setq package-archives '(("gnu"   . "http://elpa.emacs-china.org/gnu/")
			 ("melpa" . "http://elpa.emacs-china.org/melpa/"))))

;; cl - Common Lisp Extension
(require 'cl)

;; Add Packages
(defvar my/packages '(
			   ;; --- Auto-completion ---
			   company
			   ;; --- Better Editor ---
			   smooth-scrolling
			   hungry-delete
			   swiper
			   counsel
			   smartparens
			   popwin
			   ;; --- Major Mode ---
			   js2-mode
			   markdown-mode

;; ...

;; 文件末尾
(provide 'init-packages)

下面为 ~/.emacs.d/init.el 入口文件中的代码

(package-initialize)

(add-to-list 'load-path "~/.emacs.d/lisp/")

;; Package Management
;; -----------------------------------------------------------------
(require 'init-packages)

模块化要做的其实非常简单,我们要做的其实就是把某一个更改编辑器某定部分(例如,插 件管理,显示层,快捷键绑定等)的配置代码写入一个独立的文件中并在末尾为其添加 (provide 'module-name) (这里我们的模块名为 init-packages )使其可以在入口文件 中被调用,然后再在入口文件中将其引用既可。

这里需要注意的是,我们需要在入口文件中添加 (add-to-list 'load-path "~/.emacs.d/lisp/") 这可以让 Emacs 找到需要加载的模块所处的位置。

更多模块化的配置文件可以在这里找到。

Major 与 Minor Mode 详解

在这一节我们将详细介绍 Major Mode 与 Minor Mode 去区别。每一个文件类型都对应一个 Major Mode,它提供语法高亮以及缩进等基本的编辑支持功能,而 Minor Mode 则提供 其余的增强性的功能(例如 linum-mode )。

在 Emacs 中,Major Mode 又分为三种,

  • text-mode ,用于编辑文本文件
  • special-mode ,特殊模式(很少见)
  • prog-mode ,所有的编程语言的父模式

在每一个模式(mode)中它的名称与各个变量还有函数都是有特定的命名规则,比如所有的 模式都被命名为 ModeName-mode ,里面所设置的快捷键则为 ModeName-mode-map ,而所有的钩子则会被命名为 ModeName-mode-hook

注明:为了保持阅读的完整性,部分第三天的关于默认编辑器优化的内容被移至第四天。

第四天:配置文件模块化(下)以及使用优化

视频地址如下

配置文件模块化(下)

在这一部分我们首先需要知道的是什么是 features 。在 Emacs 中每一个 feature 都 是一个 Elisp 符号,用于代表一个 Lisp 插件(Package)。

当一个插件调用 (provide 'symbol_name) 函数时,Emacs 就会将这个符号加入到 features 的列表中去。你可以在这里读到更多关于 feature 的内容。

接着我们需要弄明白的是 load-file , load , require , autoload 之间的区别。 (他们之间区别的链接已经再前面贴过了,你也可以在这里找到之前同样的链接)

简单来说, load-file 用于打开某一个指定的文件,用于当你不想让 Emacs 来决定加 载某个配置文件时( .el 或者 .elc 文件)。

load 搜索 load-path 中的路径并打开第一个所找到的匹配文件名的文件。此方法用于 你预先不知道文件路径的时候。

require 加载还未被加载的插件。首先它会查看变量 features 中是否存在所要加载的 符号如果不存在则使用上面提到的 load 将其载入。(有点类似于其他编程语言中的 import

autoload 用于仅在函数调用时加载文件,使用此方法可以大大节省编辑器的启动时间。

更好的默认设置

在这一节我们会配置我们的编辑器使其有更好的使用体验。整个过程就如同搭积木一般,将 更好的体验建立在已有的功能基础之上。这样的优化使整个过程变得更高效,也更有趣。

下面的代码可以使 Emacs 自动加载外部修改过的文件。

(global-auto-revert-mode 1)

使用下面的代码可以关闭自动生产的保存文件(之前我们已经关闭过了 Emacs 自动生产的 备份文件了,现在是关闭自动保存文件)。

(setq auto-save-default nil)

如果你发现你在使用中发现了那些编辑行为与你预期的不相符时,你可以通过搜索引擎去寻 找解决方案然后将其加入你的配置中并打造一个真正属于你的神器!

popwin 插件可以自动将光标移动到,新创建的窗口中。使用下面的代码将其启用,

(require 'popwin)
(popwin-mode 1)

也许你并不喜欢听到错误时的“哔哔”的警告提示音,使用下面的代码你可以关闭 Emacs 中的警告音,

(setq ring-bell-function 'ignore)

每一次当 Emacs 需要与你确认某个命令时需要输入 (yes or no) 比较麻烦,所有我们可 以使用下面的代码,设置一个别名将其简化为只输入 (y or n)

(fset 'yes-or-no-p 'y-or-n-p)

代码缩进

indent-region 可以帮我们重新缩进所选区域的代码,但是每一次都选中十分麻烦。使用 下面的代码可以一次重新缩进全部缓冲区的代码。(之后也会介绍更好用的,代码格式美化 的插件)

(defun indent-buffer()
  (interactive)
  (indent-region (point-min) (point-max)))

(defun indent-region-or-buffer()
  (interactive)
  (save-excursion
    (if (region-active-p)
	(progn
	  (indent-region (region-beginning) (region-end))
	  (message "Indent selected region."))
      (progn
	(indent-buffer)
	(message "Indent buffer.")))))

然后再用下面的代码将其绑定为快捷键,第一个 \ 用于将紧跟的 \ 进行转义(escape)。

(global-set-key (kbd "C-M-\\") 'indent-region-or-buffer)

缩写补全

使用下面的代码我们可以开启 abbrev 模式并定义一个缩写表,每当我们输入下面的缩写 并以空格结束时,Emacs 就会将其自动展开成为我们所需要的字符串。

(setq-default abbrev-mode t)
(define-abbrev-table 'global-abbrev-table '(
					    ;; Shifu
					    ("8zl" "zilongshanren")
					    ;; Tudi
					    ("8lxy" "lixinyang")
					   ))

上面的缩写前使用的 8 也类似于命名空间的作用,使其不会与我们所常用的字符串冲突。

Hippie 补全

Company 有时候补全功能并不是非常理想,这时就可以使用 Hippie Expand 来完成补全。 Company Mode 补全效果不理想的原因是在不同的区域中会使用不同的后端函数来完成补全, 但是当后端补全函数不能被激活时,则补全就不会被激活。

我们可以将下面的代码加入到我们的配置文件中,来增强 Hippie Expand 的功能,

(setq hippie-expand-try-function-list '(try-expand-debbrev
					try-expand-debbrev-all-buffers
					try-expand-debbrev-from-kill
					try-complete-file-name-partially
					try-complete-file-name
					try-expand-all-abbrevs
					try-expand-list
					try-expand-line
					try-complete-lisp-symbol-partially
					try-complete-lisp-symbol))

然后将其绑定为快捷键,使我们可以更方便的使用它。

(global-set-key (kbd "s-/") 'hippie-expand)

Dired Mode

Dired Mode 是一个强大的模式它能让我们完成和文件管理相关的所有操作。

使用 C-x d 就可以进入 Dired Mode,这个模式类似于图形界面系统中的资源管理器。你 可以在其中查看文件和目录的详细信息,对他们进行各种操作,甚至复制粘贴缓冲区中的内 容。下面是一些常用的操作(下面的所有键均需在 Dired Mode 下使用),

  • + 创建目录
  • g 刷新目录
  • C 拷贝
  • D 删除
  • R 重命名
  • d 标记删除
  • u 取消标记
  • x 执行所有的标记

这里有几点可以优化的地方。第一是删除目录的时候 Emacs 会询问是否递归删除或拷贝, 这也有些麻烦我们可以用下面的配置将其设定为默认递归删除目录(出于安全原因的考虑, 也许你需要保持此行为。所有文中的配置请务必按需配置)。

(setq dired-recursive-deletes 'always)
(setq dired-recursive-copies 'always)

第二是,每一次你输入一个回车进入一个新的目录中时,一个新的缓冲区就会被建立。这使 得我们的缓冲区列表中充满了大量没有实际意义的记录。我们可以使用下面的代码,让 Emacs 重用唯一的一个缓冲区作为 Dired Mode 显示专用缓冲区。

(put 'dired-find-alternate-file 'disabled nil)

;; 主动加载 Dired Mode
;; (require 'dired)
;; (defined-key dired-mode-map (kbd "RET") 'dired-find-alternate-file)

;; 延迟加载
(with-eval-after-load 'dired
    (define-key dired-mode-map (kbd "RET") 'dired-find-alternate-file))

使用延迟加载可以使编辑器加载速度有所提升。

启用 dired-x 可以让每一次进入 Dired 模式时,使用新的快捷键 C-x C-j 就可以进 入当前文件夹的所在的路径。

(require 'dired-x)

使用 (setq dired-dwin-target 1) 则可以使当一个窗口(frame)中存在两个分屏 (window)时,将另一个分屏自动设置成拷贝地址的目标。

最后如果你是 Mac OS X 的用户,可以安装 reveal-in-osx-finder 这个插件(你可以在 这里找到它),它可以将任意文件直接在 Finder 中打开。你想安装这个插件,将其添加至 第二天的插件列表中即可,下次启动 Emacs 时,它就会自动帮你完成下载。

Org-mode 管理 Emacs 配置

Org-mode 下的文学编程将颠覆你对于 Emacs 的看法。因为我们也可以使用 Org 来管理 Emacs 的配置文件(笔者和他的师傅其实更倾向于模块管理配置文件)。

你需要将下面的代码放入配置入口文件( init.el )中,

(package-initialize)

(require 'org-install)
(require 'ob-tangle)
(org-babel-load-file (expand-file-name "org-file-name.org" user-emacs-directory))

之后我们需要做的仅仅只是将所有的配置文件放入 Org 模式中的代码块即可,并使用目录 结构来表述你的配置文件再把它保存在与入口文件相同的目录中即可(文件名为 org-file-name.org )。Emacs 会提取其中的配置并使其生效。这样做的好处是可以使自 己和他人更直观的,理解你的配置文件或者代码。

第五天:打造前端开发神器

视频地址如下

照例我们先修复一些现在存在的小问题。首先是自动配对的小问题,在 Emacs Lisp 中我们 有时候只需要一个 ' 但是 Emacs 很好心的帮我们做了补全,但这并不是我们需要的。我 们可以通过下面的代码来让使 Emacs Lisp 在 Emacs 中的编辑变得更方便(可以将其添加 至 init-default.el 配置文件中)。

(sp-local-pair 'emacs-lisp-mode "'" nil :actions nil)
(sp-local-pair 'lisp-interaction-mode "'" nil :actions nil)

;; 也可以把上面两句合起来
(sp-local-pair '(emacs-lisp-mode lisp-interaction-mode) "'" nil :actions nil)

在添加配置代码后重启 Emacs 使其生效。当然这个方法你也可以运用在其他的各个 Major Mode 中,如果你不想 Emacs 对某些符号进行类似的自动匹配补全。

show-paren-mode 可以使鼠标在括号上是高亮其所匹配的另一半括号,然而我们想要光标 在括号内时就高亮包含内容的两个括号,使用下面的代码就可以做到这一点。

(define-advice show-paren-function (:around (fn) fix-show-paren-function)
  "Highlight enclosing parens."
  (cond ((looking-at-p "\\s(") (funcall fn))
	(t (save-excursion
	     (ignore-errors (backward-up-list))
	     (funcall fn)))))

Lisp 的宏(Macro)类似于 C++ 中的模板,并可以生产新的代码(你可以在这里找到更多 关于宏的讨论)。使用它,我们可以增强某个函数的功能而不去更改这个函数的代码。

还有一个小问题就是解决在不同系统中的换行符,例如在 DOS 系统下的 \r(^M) 换行符, 这让我们有时候在 Unix 系统中很是头疼,因为它的存在会使版本控制误以为整行的代码都 被修改过而造成不必要的麻烦。(你可以在这里找到更多关于 \r(^M) 的信息)

我们用两种方式来处理这个问题,隐藏这个换行符或者将其删除。首先下面是隐藏的方法,

(defun hidden-dos-eol ()
  "Do not show ^M in files containing mixed UNIX and DOS line endings."
  (interactive)
  (unless buffer-display-table
    (setq buffer-display-table (make-display-table)))
  (aset buffer-display-table ?\^M []))

使用下面的代码则可以定义函数将此换行符删除,

(defun remove-dos-eol ()
  "Replace DOS eolns CR LF with Unix eolns CR"
  (interactive)
  (goto-char (point-min))
  (while (search-forward "\r" nil t) (replace-match "")))

web-mode

Emacs 自带的 HTML Mode 使用起来并不是那么的方便,而 web-mode 则是一个非常常用也 很强大的用于编辑前端代码的 Major Mode(你可以在这里找到更多关于它的信息)。

首先我们需要安装它,照例我们需要将其添加至我们的插件列表中去。

(defvar xinyang/packages '(
			   ;; 你其他的插件在这里
			   web-mode
			   ) "Default packages")

在安装完成后我们就可以开始配置它了,首先我们需要做的是将所有的 *.html 文件都使 用 web-mode 来打开。

(setq auto-mode-alist
      (append
       '(("\\.js\\'" . js2-mode))
       '(("\\.html\\'" . web-mode))
       auto-mode-alist))

这样所有的 HTML 代码在 Emacs 中就会之间启用 web-mode 而非默认的 HTML Mode 了。你 可以阅读它的文档来学习更多 web-mode 详细的使用方法。

例如使用 M-; 就可以注释当前行代码或选中行的代码。

接下来我们来做更多细节的配置,首先是缩减的大小的设置。因为 web-mode 支持在 HTML 文件中存在多语言,所以我们可以对不同的语言的缩减做出设置。下面的代码用于设置初始 的代码缩进,

(defun my-web-mode-indent-setup ()
  (setq web-mode-markup-indent-offset 2) ; web-mode, html tag in html file
  (setq web-mode-css-indent-offset 2)    ; web-mode, css in html file
  (setq web-mode-code-indent-offset 2)   ; web-mode, js code in html file
  )
(add-hook 'web-mode-hook 'my-web-mode-indent-setup)

下面的函数可以用于在两个空格和四个空格之间进行切换,

(defun my-toggle-web-indent ()
  (interactive)
  ;; web development
  (if (or (eq major-mode 'js-mode) (eq major-mode 'js2-mode))
      (progn
	(setq js-indent-level (if (= js-indent-level 2) 4 2))
	(setq js2-basic-offset (if (= js2-basic-offset 2) 4 2))))

  (if (eq major-mode 'web-mode)
      (progn (setq web-mode-markup-indent-offset (if (= web-mode-markup-indent-offset 2) 4 2))
	     (setq web-mode-css-indent-offset (if (= web-mode-css-indent-offset 2) 4 2))
	     (setq web-mode-code-indent-offset (if (= web-mode-code-indent-offset 2) 4 2))))
  (if (eq major-mode 'css-mode)
      (setq css-indent-offset (if (= css-indent-offset 2) 4 2)))

  (setq indent-tabs-mode nil))

(global-set-key (kbd "C-c t i") 'my-toggle-web-indent)

js2-refactor

js2-refactor 是一个用于重构 JavaScript 的插件,它是一个 Minor Mode,你可以在 GitHub 找到更多关于这个插件的信息。

我们使用刚刚所提到的方法来安装 js2-refactor 插件。

在安装完成后,添加一个钩子(Hook):

(add-hook 'js2-mode-hook #'js2-refactor-mode)
(js2r-add-keybindings-with-prefix "C-c C-m")

我们可以使用 C-c C-m 然后输入功能前缀,例如 em 是 extract-method 的前缀。更 多的功能和使用方法也可以在上面给出的链接中找到,所有的前缀也可以在这里找到。

优化 occur 与 imenu

下面的代码用于配置 Occur Mode 使其默认搜索当前被选中的或者在光标下的字符串:

(defun occur-dwim ()
  "Call `occur' with a sane default."
  (interactive)
  (push (if (region-active-p)
	    (buffer-substring-no-properties
	     (region-beginning)
	     (region-end))
	  (let ((sym (thing-at-point 'symbol)))
	    (when (stringp sym)
	      (regexp-quote sym))))
	regexp-history)
  (call-interactively 'occur))
(global-set-key (kbd "M-s o") 'occur-dwim)

dwim 是按我说的做的缩写(Do what I mean)。

Occur 可以用于显示变量或函数的定义,我们可以通过 popwin 的 customize-group 将定 义显示设置为右边而不是默认的底部( customize-group > popwin > Popup Window Position 设置为 right),也可以在这里对其宽度进行调节。

Occur 与普通的搜索模式不同的是,它可以使用 Occur-Edit Mode (在弹出的窗口中按 e 进入编辑模式) 对搜索到的结果进行之间的编辑。

imenu 可以显示当前所有缓冲区的列表,下面的配置可以让其拥有更精确的跳转,

(defun js2-imenu-make-index ()
      (interactive)
      (save-excursion
	;; (setq imenu-generic-expression '((nil "describe\\(\"\\(.+\\)\"" 1)))
	(imenu--generic-function '(("describe" "\\s-*describe\\s-*(\\s-*[\"']\\(.+\\)[\"']\\s-*,.*" 1)
				   ("it" "\\s-*it\\s-*(\\s-*[\"']\\(.+\\)[\"']\\s-*,.*" 1)
				   ("test" "\\s-*test\\s-*(\\s-*[\"']\\(.+\\)[\"']\\s-*,.*" 1)
				   ("before" "\\s-*before\\s-*(\\s-*[\"']\\(.+\\)[\"']\\s-*,.*" 1)
				   ("after" "\\s-*after\\s-*(\\s-*[\"']\\(.+\\)[\"']\\s-*,.*" 1)
				   ("Function" "function[ \t]+\\([a-zA-Z0-9_$.]+\\)[ \t]*(" 1)
				   ("Function" "^[ \t]*\\([a-zA-Z0-9_$.]+\\)[ \t]*=[ \t]*function[ \t]*(" 1)
				   ("Function" "^var[ \t]*\\([a-zA-Z0-9_$.]+\\)[ \t]*=[ \t]*function[ \t]*(" 1)
				   ("Function" "^[ \t]*\\([a-zA-Z0-9_$.]+\\)[ \t]*()[ \t]*{" 1)
				   ("Function" "^[ \t]*\\([a-zA-Z0-9_$.]+\\)[ \t]*:[ \t]*function[ \t]*(" 1)
				   ("Task" "[. \t]task([ \t]*['\"]\\([^'\"]+\\)" 1)))))
(add-hook 'js2-mode-hook
	      (lambda ()
		(setq imenu-create-index-function 'js2-imenu-make-index)))

(global-set-key (kbd "M-s i") 'counsel-imenu)

expand-region

使用同样的方法将 expand-region 添加至我们的插件列表中,重启 Emacs 安装插件。

再为其绑定一个快捷键,

(global-set-key (kbd "C-=") 'er/expand-region)

使用这个插件可以使我们更方便的选中一个区域。(更多使用方法和文档可以在这里找到)

iedit

iedit 是一个可以同时编辑多个区域的插件,它类似 Sublime Text 中的多光标编辑。它的 GitHub 仓库在这里

我们将其绑定快捷键以便更快捷的使用这个模式( C-; 为默认快捷键),

(global-set-key (kbd "M-s e") 'iedit-mode)

我们可以使用 Customized-group 来更改其高亮的背景色,将 highlight 改为 region

Org 导出

使用 C-c C-e 可以将 Org-mode 文档导出为你需要的格式,例如 HTML 或者 PDF 文件。 你现在看到的这本教程就是由 Org-mode 所导出生成的。

第六天:代码片段与语法检查器

视频地址如下

在开始前我们需要注意的是之前模块化的配置文件 init-keybindings.el 应该放在所有 调用模块的最后面,因为也许在设置快捷键时某些函数还未被加载。

我们可以使用下面的配置来在 Company-mode 中使用 C-nC-p 来选择补全项,

(with-eval-after-load 'company
  (define-key company-active-map (kbd "M-n") nil)
  (define-key company-active-map (kbd "M-p") nil)
  (define-key company-active-map (kbd "C-n") #'company-select-next)
  (define-key company-active-map (kbd "C-p") #'company-select-previous))

Org-mode 进阶使用

在学习代码片段和语法检查器(Linter)之前,我们先来学习一下如何使用 Org-mode 来做 学习笔记和安排工作时间。我们用下面的配置代码来设置一个模板(其中设置了待办事项的 优先级还有触发键),

(setq org-capture-templates
      '(("t" "Todo" entry (file+headline "~/.emacs.d/gtd.org" "工作安排")
	 "* TODO [#B] %?\n  %i\n"
	 :empty-lines 1)))

我们也可以为其绑定一个快捷键,

(global-set-key (kbd "C-c r") 'org-capture)

这个功能除了可以记录待办事项还有其他许许多多的功能例如获取将当前浏览器中的 URL(下面的例子只在 Mac OS X 平台有效)。

(defun YOUR_NAME/retrieve-chrome-current-tab-url()
  "Get the URL of the active tab of the first window"
  (interactive)
      (let ((result (do-applescript
		     (concat
		      "set frontmostApplication to path to frontmost application\n"
		      "tell application \"Google Chrome\"\n"
		      " set theUrl to get URL of active tab of first window\n"
		      " set theResult to (get theUrl) \n"
		      "end tell\n"
		      "activate application (frontmostApplication as text)\n"
		      "set links to {}\n"
		      "copy theResult to the end of links\n"
		      "return links as string\n"))))
	(format "%s" (s-chop-suffix "\"" (s-chop-prefix "\"" result)))))

更多有关 Org-capture 的内容可以在这里找到。

Org-pomodoro 是一个番茄时间工作法的插件(更多关于这个工作法的信息可以在这里找到)。 它的 GitHub 地址在这里。在 (require 'org-pomodoro) 后可以通过 Customize-group 来对其进行设置,包括不同休息种类的时长。

因为每次保存中文的时候都需要选择解码,我们可以使用下面的配置将文本解码设置默认为 UTF-8,

(set-language-environment "UTF-8")

当 org-mode 不能生效时,我们需要将与 Org 相关的配置放置于 with-eval-after-load 中,

(with-eval-after-load 'org
  ;; Org 模式相关设定
  )

批量修改文件名

C-x C-q 就可以直接在 Dired Mode 中进行编辑,使用之前学的 iedit-mode 和区域选择 就可以直接对多个文件进行重命名编辑了。

搜索与替换

全局搜索在我们的编辑工作中是不可缺少的,今天我们介绍的是 ag。它是非常快速的命令 行搜索工具,它是 Linux 的所有搜索工具中最快的。

ag > pt > ack > grep

在使用 ag 前我们需要进行安装,下面是 Mac OS X 与 Ubuntu 下的安装方法,

# Mac OS X 通过 Homebrew 安装
brew install the_silver_searcher

# Ubuntu 下安装
apt-get install silversearcher-ag

# Windows 下通过 msys2 安装(确保在 path 中)
pacman -S mingw-w64-i686-ag # 32 位电脑
pacman -S mingw-w64-x86_64-ag # 64 位电脑

安装好 ag 后我们就可以安装 helm-ag 插件了。(它的 GitHub 仓库地址在这里)在安装 完成后可以为其设置快捷键,

(global-set-key (kbd "C-c p s") 'helm-do-ag-project-root)

使用这个插件我们同样可以在缓冲区对搜索到的结果进行直接的修改,这样就可以做到快速 的搜索与替换。

语法检查器(Linter)

语法检查器可以在开发动态语言(Interpreted/Dynamic Programming Language)时极大的 提高你的开发效率,它会实时的检查你的代码并将句法错误(Syntax Error)与静态语义 (Static Semantic Error)错误进行高亮与提示。

我们在这里使用的例子是 JavaScript 的语法检查器 eslint 它的安装方法可以在这里找到。

在安装好语法检查器后就可以安装 flycheck 的插件了,它的 GitHub 的地址在这里

使用下面的代码可以将 flycheck-mode 在特定模式下激活(下面的例子就是只有在打开 JavaScript 时才会激活语法检查器),

(add-hook 'js2-mode-hook 'flycheck-mode)

使用 flycheck-verify-setup 可以进行语法检查器的选择。

eslint 检查器的配置也可以使用项目目录下的 .eslintrc 来进行配置,更多配置方法可 以在这里找到。

代码块

yasnippet 是一个代码块补全的插件(GitHub 地址)。使用下面的配置文件将其在所有 的编程语言的模式中激活。

(yas-reload-all)
(add-hook 'prog-mode-hook #'yas-minor-mode)

自定义代码块的方法可以在上面提供的链接中找到。

auto-yasnippet

auto-yasnippet 也是一个非常好用代码块补全插件。安装并未其设置快捷键,

(global-set-key (kbd "H-w") #'aya-create)
(global-set-key (kbd "H-y") #'aya-expand)

简单的使用方法就是使用 ~ 来定义模板,然后调用 aya-create 再使用 aya-expand 来使用模板。

第七天:Evil 模式

视频地址如下,

开始之前我们先将 C-w 来使其可以向后删除一个单词,这样就可以与 Shell 中的快捷键操作同步。

(global-set-key (kbd "C-w") 'backward-kill-word)

如果你不是 Vim 的用户,你可以选择跳过这一天的内容。但是我们强烈建议你花一些时间 来学习基本的 Vim 操作,即使 Emacs 是你的最爱 Vim 的快捷键也在一定程度会增加你的 编辑效率。基础的 Vim 操作可以在这里学会。

Evil 模式中的 State 就相当与 Vim 中的模式,常用的模式有下面几种(后面对于了相应 的 Emacs 中的 State),

  • Normal Mode -> Normal State
  • Insert Mode -> Insert State
  • Visual Mode -> Visual State
  • Motion Mode -> Motion State

还有一个 Emacs 中的特殊状态是 Emacs State。

Evil 的安装

照例我们需要将 Evil 插件添加至我们的插件列表中来完成安装。在重启 Emacs 完成安装 后可以添加下面的代码将其激活。

(evil-mode 1)

在激活 Evil 模式后就可以,在 Emacs 中使用 Vim 的快捷键了。有一点需要注意 C-u 在 Emacs 中有特殊的功所(Universal args)以能我们可以通过使用 customize-group 来对 Evil 模式进行修改,将 Evil Want C U Scroll 设置为开启。

下面的代码可以将 insert state map 中的快捷键清空,使其可以回退(Fallback)到 Emacs State 中,这样我们之前的 Emacs State 里面定义的 C-w 等快捷键就不会被 evil insert minor mode state 所覆盖,

(setcdr evil-insert-state-map nil)
(define-key evil-insert-state-map [escape] 'evil-normal-state)

这样你就可以使用 Evil 来在 Emacs 中完成百分之八十作用的快捷键操作了。

完整的 Evil Mode 的 PDF 版本的操作指南可以在这里找到,建议从 Vim 转 Emacs 的用户 多次阅读来完整的掌握这个模式的使用方法。

Evil 模式插件

Leader Key

在 Emacs 中使用 Leader Key 可以通过 evil-leader 来实现。你需要做的就是在安装后将其激活即可。

提示: 根据cofi/evil-leader 的说明,你应该在激活 evil-mode 之前就激活 global-evil-leader-mode,否则 evil-leader 在几个初始缓冲区(scratch, Message,…)上将不生效。

(global-evil-leader-mode)

Leader Key 可以通过 customize-group 来进行设置(Evil Leader/Leader)。因为之后我 们会转移至 Spacemacs 所以我们可以将其设置为空格键 SPC

在通过下面的配置来设置简单的结合 Leader Key 快捷键组合(我们使用不同的键讲不同的 功能分组,例如我们使用 f 键来做关于文件的操作,使用 b 键来做关于缓冲区 (Buffer)的操作),

(evil-leader/set-key
  "ff" 'find-file
  "bb" 'switch-to-buffer
  "0"  'select-window-0
  "1"  'select-window-1
  "2"  'select-window-2
  "3"  'select-window-3
  "w/" 'split-window-right
  "w-" 'split-window-below
  ":"  'counsel-M-x
  "wM" 'delete-other-windows
  )

注意上面窗口跳转相关的设置需要 window-numbering 安装后方可生效。

Window-numbering

这个插件可以让我们快速的使用 Leader Key 与数字键的组合来在多个窗口之间进行跳转。 它的 GitHub 地址可以在这里找到。

下载安装后通过下面的代码可以将其激活,

(window-numbering-mode 1)

Evil-Surround

Evil-surround 是一个 Vim 上非常常用的插件改写的,使用它可以快速的将选中区域进行 匹配的操作,例如选中区域两边同时进行添加或修改括号,引号等操作。

下载安装后使用下面的代码将其激活,

(require 'evil-surround)
(global-evil-surround-mode)

简单的使用方法就是在选中所选区域后,使用 S( 来将选中区域包括在括号之中。如果想 将括号改变成 " 可以在选中后使用 cs("

Evil-nerd-commenter

这是一个快速添加与取消注释的插件,它的 GitHub 地址在这里

使用下面代码可以将其激活,

(evilnc-default-hotkeys)

使用下面的代码将其与 Evil 模式进行绑定,这里我们选择使用 ,/ 作为快捷键。

(define-key evil-normal-state-map (kbd ",/") 'evilnc-comment-or-uncomment-lines)
(define-key evil-visual-state-map (kbd ",/") 'evilnc-comment-or-uncomment-lines)

Which-key

which-key 可以显示当前组合键下所有可以使用的全部组合键的选项。使用这个插件可以很 好的解决快捷键太多的问题,我们无需在记忆任何快捷键,而是根据自己的需求使用不同分 组的快捷键后再在其中去需找自己需要的功能。

在下载后可以使用下面的代码进行激活,

(which-key-mode 1)

Org Mode 中的搜索

C-c a 中可以根据提示使用 s 来进行关键字所搜。使用 t 则可以进行代办事项的搜索。

第八天:Cask 介绍与实践

视频地址如下:

什么是 Cask:

Cask 是一个 Emacs Lisp 的项目管理工具.

Cask 的安装

Cask 的地址: Cask

可以使用如下命令安装 Cask:

curl -fsSL https://raw.githubusercontent.com/cask/cask/master/go | python

安装过程中 Cask 将会进行 Bootstrap, 安装一些包到~/.emacs.d 目录中.

安装完成之后需要将 Cask 的可执行文件目录加入到 PATH 中.

可以看到, Cask 的安装过程在 ~/.emacs.d 中新建了一个 .Cask 目录.

安装 Pallet: Pallet 是一个基于 Cask 的包管理工具, 可以使用 Emacs 的包安装方式安装.

然后使用以下代码启用 Pallet:

(pallet-mode)  
(pallet-init)    ; 在.emacs.d 中生成一个 Cask 文件, 写入源与现有包
(pallet-install) ; 将 elpa 中的 package 拷贝到.Cask/<you version>/elpa 目录中

然后在配置文件中加入以下代码(在其他包加载之前) :

(require 'cask "<path-to-cask>/cask.el")
(cask-initialize)    ; 类似于 package-initialize
(require 'pallet)
(pallet-mode t)      ; 激活 pallet, 在安装包时将 Cask 文件写入相应信息

具体使用方式见文档: Pallet

为何使用 Cask

  • 使用 Cask 文件保存包的信息, 可以通过 Cask install 自动安装
  • 当有多个版本的 Emacs 时, 将各版本的包单独存放

故障排除

Emacs24.5 时不能安装 let-alist

解决方案: 将 Gnu 的源加入源列表中, 示例代码如下(加入 Cask 文件)

(source "gnu" "http://elpa.zilongshanren.com/gnu/")

在 Emacs24.5 中没有定义 define-advice

define-advice 是一个 Emacs25 中加入的宏, 在 Emacs24 中不能使用, 可以使用 defadvice 代替:

(defadvice show-paren-function (around fix-show-paren-function activate)
  (cond ((looking-at-p "\\s(") ad-do-it)
	(t (save-excursion
	     (ignore-errors (backward-up-list))
	     ad-do-it)))
  )

而且使用 defadvice 相比 define-advice 还有一个优点:

使用 define-advice 定义的代码, 当在 Normal 模式时光标两边的括号不会高亮, 而使用 defadvice 可以.

第九天:Macro 与 Use-package

视频地址如下:

这次主要介绍 Emacs Lisp 中的宏(Macro)以及 Use-package 插件.

Macro

什么是 Macro

宏是一种可以生成代码的代码. 类比与 C 语言中的宏以及 C++中的模板. 先看一个简单的例子:

(defmacro inc (var)
  (list 'setq var (list '1+ var)))

(setq my-var 1)
(setq my-var (+ 1 my-var))

(macroexpand '(inc my-var))

以上这个宏的作用是将变量的值+1. 执行以上代码之后, my-var 的结果为 2.

可以使用 macroexpand 获得宏展开的结果, 如以上代码结果为:

(setq my-var (1+ my-var))

我们也可以使用函数来实现相同的功能, 但 Macro 与函数有以下两个区别:

  1. 宏的参数并不会被马上求值, 解释器会先展开宏, 宏展开之后解释器才会执行宏展开的 结果; 而函数的参数会马上求值
  2. 宏的执行结果是一个表达式, 该表达式会立即被解释器执行; 而函数的结果是一个值

backquote

backquote 是指反引号(`), 即键盘上数字 1 左边的键.

当在 Emacs 输入 backquote 时会插入两个反引号, 可以使用以下代码关闭这个功能:

(sp-local-pair 'emacs-lisp-mode "`" nil :actions nil)

先看以下例子来体会 backquote 的作用:

(defun my-print (number)
  (message "This is a number: %d" number))

(my-print 2)               ; 1. output= This is a number: 2
(my-print (+ 2 3))         ; 2. output= This is a number: 5

(quote (+ 1 1))
;; return a list= (+ 1 1)

(defmacro my-print-2 (number)
  `(message "This is a number: %d" ,number))

(my-print-2 2)             ; 3. output= This is a number: 2
(my-print-2 (+ 2 3))       ; 4. output= This is a number: 5

(setq my-var 2)
(inc my-var)

(defmacro inc2 (var1 var2)
  (list 'progn (list 'inc var1) (list 'inc var2)))

(macroexpand '(inc2 my-var my-var))
(macroexpand-all '(inc2 my-var my-var))

quote 的作用是返回后面的表达式, 不对表达式进行求值. 所以以下代码:

(quote (+ 1 1))
;; return a list= (+ 1 1)

并没有对表达式 (+ 1 1) 进行求值返回 2, 而是返回一个 list.

backquote 的作用与 quote 相似, 同样不对后面的表达式求值, 但是当 backquote 在宏中 与逗号(,)一起使用时, 用逗号修饰的变量将进行求值.

例如以下代码:

(defmacro my-print-2 (number)
  `(message "This is a number: %d" ,number))

(pp (macroexpand '(my-print-2 (+ 2 3))))
(my-print-2 (+ 2 3))

当输出 message 且 number 不带逗号时, my-print-2 的执行将提示错误. 因为宏不对参 数进行求值, 所以以上宏展开相当于:

(message "This is a number:" number)

因为我们没有定义 number 变量, 所以执行出错.

而如果加入逗号, 则在宏展开时会对变量 number 进行求值, 展开结果为:

(message "This is a number: %d" (+ 2 3))

在调试宏的过程中, 可以使用 macroexpandmacroexpand-all 获取宏展开的结果.

关于 backquote 的更多讨论, 可以见以下地址: lisp 中的`与,是怎么用的?

为什么使用宏

使用宏可以减少重复的代码, 以下是一个使用宏来定义函数的例子:

(defun prelude-search (query-url prompt)
  "Open the search url constructed with the QUERY-URL.
PROMPT sets the `read-string prompt."
  (browse-url
   (concat query-url
	   (url-hexify-string
	    (if mark-active
		(buffer-substring (region-beginning) (region-end))
	      (read-string prompt))))))

(defmacro prelude-install-search-engine (search-engine-name search-engine-url search-engine-prompt)                   ; #1
  "Given some information regarding a search engine, install the interactive command to search through them"    
  `(defun ,(intern (format "prelude-%s" search-engine-name)) ()                                                       ; #2
       ,(format "Search %s with a query or region if any." search-engine-name)                                        ; #3
       (interactive)
       (prelude-search ,search-engine-url ,search-engine-prompt)))                                                    ; #4

(prelude-install-search-engine "google"     "http://www.google.com/search?q="              "Google: ")                ; #5
(prelude-install-search-engine "youtube"    "http://www.youtube.com/results?search_query=" "Search YouTube: ")
(prelude-install-search-engine "github"     "https://github.com/search?q="                 "Search GitHub: ")
(prelude-install-search-engine "duckduckgo" "https://duckduckgo.com/?t=lm&q="              "Search DuckDuckGo: ")

下面对以上代码进行讲解:

第#1 行, 通过 prelude-install-search-engine 定义了一个需要 3 个参数的宏, 这个 宏的作用是生成一个函数.

第#2 行, 通过 intern 生成一个符号作为函数名, 名称为 prelude-xxx , 其中 xxx 为第一个参数的值.

第#3 行, 生成了这个函数的描述.

第#4 行, 调用 prelude-search 函数进行搜索处理.

第#5 行, 调用这个宏定义了一个名为 prelude-google 的函数.

从以上代码可以知道, 我们利用宏生成了4个名称不同的函数, 避免了手动编写函数的问题 (因为这4个函数的代码非常相似, 根据 DRY 原则应该尽量避免做这种重复工作).

关于宏的更多内容, 可以阅读 Paul Graham 的著作 《On Lisp》

prelude-duckduckgo

Use-package

简介

Use-package 是一个宏, 它能让你将一个包的 require 和它的相关的初始化等配置组织 在一起, 避免对同一个包的配置代码散落在不同的文件中.

Use-package 的更多信息参见以下地址: Use-package

一些简单的用法

  • 更安全的 require

    在 Emacs 中, 当我们要引入一个包时, 通常会使用以下代码:

    (require 'package-name)
    
    

    但是当 package-name 不在 load-path 中时, 以上代码会抛出错误. 使用 Use-package 可以避免:

    (use-package package-name)
    
    

    以上代码展开的结果如下:

    (if
        (not
         (require 'package-name nil 't))
        (ignore
         (message
          (format "Cannot load %s" 'package-name))))
    
    

    可以看到, Use-package 使用 ignore 来避免抛出错误, 这样当某个包不存在时, eamcs 也能够正常启动.

  • 将配置集中

    当我们引入某个包时, 有可能需要定义一些与这个包相关的变量, 使用 Use-package 实 现这个需求如下:

    (use-package package-name
      :init
      (setq my-var1 "xxx")
      :config
      (progn
        (setq my-var2 "xxx")
        (setq my-var3 "xxx")
        )
      )
    
    

    在上例中, init 后的代码在包的 require 之前执行, 如果这段代码出错则跳过包的 require.

    config 后的代码在包的 require 之后执行.

    initconfig 之后只能接单个表达式语句, 如果需要执行多个语句, 可以用 progn .

  • autoload

    使用 require 时会引入这个包, 但是当你的包很多时会影响启动速度. 而使用 autoload 则可以在真正需要这个包时再 require, 提高启动速度, 避免无谓的 require.

    使用 Use-package 可以轻松的实现这个功能:

    (use-package package-name
      :commands
      (global-company-mode)
      :defer t
      )
    
    

    使用 commands 可以让 package 延迟加载, 如以上代码会首先判断 package 的符号是否 存在, 如果存在则在 package-name 的路径下加载. defer 也可以让 package-name 进行延迟加载.

  • 键绑定

    在之前的代码中, 如果我们需要绑定一个键, 需要使用 global-key-binddefine-key 实现, 而使用*Use-package* 实现更简单:

    (use-package color-moccur
      :commands (isearch-moccur isearch-all)
      :bind (("M-s O" . moccur)
    	 :map isearch-mode-map
    	 ("M-o" . isearch-moccur)
    	 ("M-O" . isearch-moccur-all))
      :init
      (setq isearch-lazy-highlight t)
      :config
      (use-package moccur-edit))
    
    

为什么使用 Use-package

  1. Use-package 能让相关的配置更为集中, 避免配置分散带来的维护困难
  2. Use-package 有完善的错误处理, 使配置代码更为健壮
  3. Spacemacs 也大量使用了 Use-package

第十天: Company-mode 与 auto-completion

视频地址如下:

内容概要:

  1. 给出 Cask 和 Use-package 的简单示例
  2. 详细介绍 Company-mode 的工作原理, 各种 backend 及其用法

扩展阅读:

  1. 编写一个简单的 comopany backend
  2. Company Mode Emacs wiki

升级 Package 之后有 BUG 怎么办

有些时候我们将一个 Package 升级到最新的版本(例如 github 上最新的 commit), 而该版本可能会存在一些 BUG, 这就会导致我们的配置不能使用.

如果我们使用 stable 版本的 Package(例如使用 github 上最新的 release), 就可以尽量地减少因为升级包之后的 BUG 导致配置不可用的情况.

另一种解决方式是使用 Cask 进行包管理, 举例如下:

首先我们添加一个包, 例如我们编辑 Cask 文件, 添加 monokai-theme :

(depends-on "monokai-theme"
	    :git "https://github.com/oneKelvinSmith/monokai-emacs/releases"
	    :ref "02c5f5d")

然后启动 emacs, 但是出现了 BUG. 这时我们可以直接将 ref 的值修改为最新的 commit, Cask 即会更新这个包, 而不用等待 melpa 对包进行更新.

Use-package 的更多用法

如果我们启用 exec-path-from-shell , 在 emacs 启动时可能会提示 PATH 变量重复定义, 解决方案如下:

(use-package exec-path-from-shell
  :ensure t
  :if (and (eq system-type 'darwin) (display-graphic-p))
  :config
  (progn
    (when (string-match-p "/zsh$" (getenv "SHELL"))
      ;; Use a non-interactive login shell.  A login shell, because my
      ;; environment variables are mostly set in `.zprofile'.
      (setq exec-path-from-shell-arguments '("-l")))

    (exec-path-from-shell-initialize)
    )
  )

;; (use-package monokai-theme
;; :ensure t)

其中 if 子句可以确定启用 Package 的条件, 在 config 子句中向 exec-path-from-shell-arguments 即可消除这个警告.

ensure 子句来确保 Package 被安装. 如果要使用 stable 版, 则添加以下子句:

:pin melpa-stable

Company-mode 的工作原理

Company-mode 需要配合后端使用, 所有的 backend 都保存在 company-backends 这个变量中, 例如在我的环境中该变量值如下:

(company-capf
 (company-dabbrev-code company-gtags company-etags company-keywords)
 company-files
 company-dabbrev
 )

*company-backends*变量的值是一个列表, 其中的每一项都是一个后端或 Group Backend.

Company-mode 会依次调用该变量中的 backend, 并判断该 backend 是否合适当前 Buffer, 直到找到一个合适的补全后端.

在进行补全项选择的时候, 我们也可以在 mode-line 中看到是使用的哪一个后端.

  1. company-dabbrev: 将当前打开的所有 buffer 中的关键字作为补全显示(默认不使用 scratch buffer).
  2. company-files: 补全路径.

如果在补全过程中取消了补全, 也可以使用命令再次开启补全. 例如 company-files 补全方式就可以使用 company-files 函数开启.

在输入英文时可以使用 company-ispell 进行输入提示.

为什么有时 Python 的补全 不工作

有时在编写 Python 代码的时候补全不能工作, 这时我们可以先查看 company-backends 的值, 查看是否需要安装 company-anaconda 用于补全.

在 Mac 系统中, 如果 anaconda-mode 的安装过程出现错误, 可以参照以下解决方案: Mac 上面编辑 python 的时候安装 anaconda-mode 出错

然后确保在 company-backends 中有 company-anaconda 这个后端即可. 可以使用如下代码:

(add-hook 'python-mode-hook
	  (lambda ()
	    (set (make-local-variable 'company-backends) '(company-anaconda))))

以上代码在 python-mode 被激活时设置 company-backends 的变量值为 (company-anaconda), 则在编辑 python 代码时就可以使用 anaconda-mode 进行补全.

在 emacs 中有两种补全方式:

  1. 如 company-files 等, 根据关键字等进行补全, 只需要 emacs 自己进行一些处理即可得到补全数据;
  2. 如 company-anaconda 等, 需要使用客户端-服务端模式, 补全后端需要服务端的配合才能得到补全数据.

在进行编程时, 一般使用第二种补全后端, 例如 编写 python 代码使用 company-anaconda, 编写 C/C++代码使用 company-ycmd, 编写 javascript 代码使用 company-tern.

上述示例代码开启 company-anaconda 时有一个缺点, 比如当我们在 python 的注释时没法使用补全, 因为补全后端只有 company-anaconda, 在注释时不工作. 我们可以将 dabbrev 加入后端列表:

(add-hook 'python-mode-hook
	  (lambda ()
	    (set (make-local-variable 'company-backends) '(company-anaconda company-dabbrev))))

当 company-anaconda 不合适时使用 company-dabbrev 进行补全, 即可满足上述需求.

Group Backend

company-dabbrev-code: 类似于 company-dabbrev, 但是 dabbrev 对代码和注释都进行补全, dabbrev-code 只补全代码.

假设我们的配置代码如下:

(add-hook 'python-mode-hook
	  (lambda ()
	    (set (make-local-variable 'company-backends) '((company-anaconda company-dabbrev-code)
							   company-dabbrev)))
	  )

如果我们在注释中出现了 xxx 这个字符串, 在编写 python 代码时 xxx 不会出现在补全选项中. 因为在此时会先匹配到 company-anaconda, 并不会进入 company-dabbrev. 而因为 company-anaconda 是一个 Group Backend, 所以 company-dabbrev-code 的补全数据会出现在补全列表中.

怎样写一个简单的补全后端

因为视频时间关系, 大家可以去阅读以下文章:

Writing the Simplest Emacs company-mode Backend

实现方式简单介绍如下:

  1. 定义一个补全数据列表
  2. 定义一个补全函数, 返回对应输入的补全数据

更多的内容可以查看该文档, 同时在网页右侧的链接中有更多的详细内容.

第十一天: Spacemacs 简介及安装

视频地址如下:

从今天的内容开始介绍 Spacemacs. 地址: Spacemacs

今天的内容包括:

  1. 如何安装 Spacemacs
  2. 一些简单的配置, 以及 package 管理
  3. 管理自己的配置

安装 Spacemacs

安装 Spacemacs 非常简单, 只需要将 github 上的仓库克隆即可, 即执行以下命令(如有必要可以先备份以前的配置):

cd ~
mv .emacs.d .emacs.d.bak
mv .emacs .emacs.bak
git clone https://github.com/syl20bnr/spacemacs ~/.emacs.d

在克隆完成后直接运行 Emacs. 在第一次使用 Spacemacs 时需要下载一些 Package, 然后在 Bootstrap 完成之后你需要进行如下一些配置:

  1. 使用哪种编辑方式, 包括 vim 方式(默认) 以及 emacs 方式.
  2. 使用哪种 Spacemacs distribution. 包括标准版(默认)以及基础版. 区别在于标准版包含非常多的功能, 而基础版只包含核心功能.

在完成以上两个配置之后, 就会在 HOME 目录生成一个 ~/.spacemacs 配置文件. 然后 Spacemacs 会进行进一步的初始化, 下载更多的需要的 Package. 如果你需要使用 emacs-china 的配置源, 此时可以终止 emacs, 然后在~/.spacemacs 中的 dotspacemacs/user-init 函数中加入以下代码:

(setq configuration-layer--elpa-archives
      '(("melpa-cn" . "http://elpa.zilongshanren.com/melpa/")
	("org-cn"   . "http://elpa.zilongshanren.com/org/")
	("gnu-cn"   . "http://elpa.zilongshanren.com/gnu/")))

重新启动 emacs, 等待 Spacemacs 完成安装即可.

如果你需要更方便的管理你自己的配置, 可以创建 ~/.spacemacs.d 目录, 然后将 ~/.spacemacs 文件移动到该目录中并重命名为 init.el.

在 Spacemacs 中的操作方式如下:

  1. 按下 SPC f j 打开 dired 目录
  2. 按下按键 + , 创建 ~/.spacemacs.d 目录
  3. 将光标移动到 .spacemacs 文件上, 按下 R, 将该文件移动到 .spacemacs.d 目录中
  4. 进入 .spacemacs.d 目录, 将光标移动到 .spacemacs 文件上, 按下 R, 将该文件重命名为 init.el
  5. 按下 qq 退出 dired

然后启动 emacs 即可.

使用这种方式管理配置, 你可以将自己的配置集中到 ~/.spacemacs.d 目录中, 更容易进行统一管理. 你也可以将自己的配置 push 到 github 上.

添加内置的 layer

在安装完成 Spacemacs 之后, 按下 SPC f e d 打开 ~/.spacemacs 文件, 修改 dotspacemacs-configuration-layers 变量的值, 将 auto-completion, better-defaults, emacs-lisp, git, markdown, org, spell-checking, syntax-checking 等 layer 加入列表.

然后退出 emacs 再重启, 或者按下 SPC f e R 安装需要的 package.

一些简单的配置

启动时全屏显示

在 dotspacemacs/init 函数中, 将 dotspacemacs-fullscreen-at-startup 变量设置为 t 即可. 代码如下:

;; If non nil the frame is maximized when Emacs starts up.
;; Takes effect only if `dotspacemacs-fullscreen-at-startup' is nil.
;; (default nil) (Emacs 24.4+ only)
dotspacemacs-maximized-at-startup t

ivy layer

将 ivy 加入 dotspacemacs-configuration-layers 列表中. 按下 CTRL s 使用 swiper 可以进行搜索.

查看 layer 下的 文档信息

按下 SPC h SPC 即会弹出一个信息窗口, 可以从窗口中选择具体的 layer 或者其他信息进行查看.

删除安装的 package

只需要将需要删除的 package 名称加入到 dotspacemacs-excluded-packages 变量中, 在下一次启动 emacs 时即会删除该 package. 示例代码如下:

;; A list of packages and/or extensions that will not be install and loaded.
dotspacemacs-excluded-packages '(vi-tilde-fringe)

安装 package

在 Spacemacs 中安装 package 时最好不要使用 package-install, 因为这样安装的 package 会在下一次启动时被删除.

Spacemacs 提供了一个方式, 你只需将需要安装的 package 加入到 dotspacemacs-additional-package 变量中即可, 示例代码如下:

;; List of additional packages that will be installed without being
;; wrapped in a layer. If you need some configuration for these
;; packages, then consider creating a layer. You can also put the
;; configuration in `dotspacemacs/user-config'.
dotspacemacs-additional-packages '(youdao-dictionary)

配置 customize-group

如果使用 customize-group 对配置进行了修改, 你可以以下代码将生成的 custom.el 配置文件纳入 ~/.spacemacs.d 目录中进行统一管理:

(setq custom-file (expand-file-name "custom.el" dotspacemacs-directory))
(load custom-file 'no-error 'no-message)

修改主题

只需修改 dotspacemacs-themes 变量的值, 将主题加入列表即可. 在列表中靠前的主题会优先使用. 示例代码如下:

;; List of themes, the first of the list is loaded when spacemacs starts.
;; Press <SPC> T n to cycle to the next theme in the list (works great
;; with 2 themes variants, one dark and one light)
dotspacemacs-themes '(
		      monokai
		      ;; spacemacs-dark
		      ;; spacemacs-light
		      ;; solarized-light
		      solarized-dark
		      ;; leuven
		      ;; monokai
		      ;; zenburn
		      )

第十二天: 创建你的第一个 Spacemacs Layer

视频地址如下:

主要内容:

  1. 如何更新 Spacemacs, 同步官方 develop 分支及注意事项
  2. Layer 的 variables 变量及使用方法
  3. 如何创建自己的 Layer
  4. 如何定制 modeline
  5. evlified state

如何更新 Spacemacs

可以通过 git 的方式来更新代码, 假设我们使用的是 develop 分支:

git checkout develop
git fetch upstream
git merge upstream/develop

一般来说, 如果你不熟悉 emacs 并且你的 Spacemacs 配置能够正常工作, 则不需要频繁的更新代码, 以避免更新之后配置不能使用.

variables 变量

每一个 layer 都可以配置一些变量, 可以通过 SPC h SPC 然后输入 layer 名称, 点击对应的选项即可打开该 layer 的 README.org 文件. 然后按下 SPC f j 进入 dired 模式, 选择 config.el 文件打开, 该文件中即定义了该 layer 的变量.

例如 better-default layer 的变量如下:

(defvar better-defaults-move-to-beginning-of-code-first t
  "when t, first stroke of C-a will move the cursor to the beginning of code.
When nil, first stroke will go to the beginning of line.
Subsequent strokes will toggle between beginning of line and beginning of code.")

(defvar better-defaults-move-to-end-of-code-first nil
  "when t, first stroke of C-e will move the cursor to the end of code (before comments).
When nil, first stroke will go to the end of line (after comments).
Subsequent strokes will toggle between end of line and end of code.")

要配置使用这些变量, 可以在启用 layer 时使用如下的代码:

(better-defaults :variables
		 better-defaults-move-to-end-of-code-first t)

定制 modeline

在 emacs25.1 中, 该版本的 modeline 和以前版本不同, 可以通过如下方式将 modeline 修改为以前的显示形状:

在 dotspacemacs/user-config 中加入如下代码:

(setq ns-use-srgb-colorspace nil)

创建自己的 layer

假设我们需要创建一个 layer, 名叫 zilongshanren, 并且在 layer 下包含一个名叫 youdao-dictionary 的 package.

首先利用 spacemacs 提供的函数创建 layer. 按下 M-x 并且输入 configuration-layer/create-layer, 然后选择路径 ~/.spacemacs.d, 确定创建 README, 然后我们就可以看到 layer 创建成功.

每一个 layer 的结构如下:

[layer_name]
  |__ [local]
  | |__ [package 1]
  | |     ...
  | |__ [package n]
  |-- layers.el
  |__ packages.el
  |__ funcs.el
  |__ config.el
  |__ keybindings.el

[] = directory

即每一个 layer 目录下都可以包含 layers.el, packages.el 等文件, 以及一个名叫 local 的目录.

每一个文件的内容描述如下:

文件名 用处
layers.el 申明一些额外的 layer 依赖
packages.el 一些 layer 使用到的 package 以及相关配置函数
funcs.el 定义一些 layer 层次的函数, 即全局函数
config.el layer 的配置, 此处定义的配置可以在 .spacemacs 中申明 layer 时进行配置, 也可以定义 emacs 的默认配置
keybindings.el 快捷键配置

现在我们可以把 youdao-dictionary 加入到 layer 中, 编辑 packages.el:


;; 添加 package
(defconst zilongshanren-packages
  '(youdao-dictionary)
  )

;; 初始化 package
;; 可以使用 , d m 快捷键, 然后按下 e 展开宏
(defun zilongshanren/init-youdao-dictionary ()
  (use-package youdao-dictionary
    :defer t
    :init
    (spacemacs/set-leader-keys "oy" 'youdao-dictionary-search-at-point+)
    )
  )

编辑 config.el 文件:

;; 开启行号显示
(global-linum-mode t)

;; 定义快捷键
(global-set-key (kbd "M-s o") 'occur-dwim)

;; 将 occur 的 buffer 中的光标移动方式修改为 HJKL
(evilified-state-evilify-map occur-mode-map
  :mode occur-mode)

编辑 keybindings.el 文件:

;; dwin = do what i mean.
(defun occur-dwim ()
  "Call `occur' with a sane default."
  (interactive)
  (push (if (region-active-p)
	    (buffer-substring-no-properties
	     (region-beginning)
	     (region-end))
	  (let ((sym (thing-at-point 'symbol)))
	    (when (stringp sym)
	      (regexp-quote sym))))
	regexp-history)
  (call-interactively 'occur))

然后将 zilongshanren 加到 dotspacemacs-configuration-layers 变量中, 即可让 layer 配置生效.

文档

spacemacs 的文档保存在 doc 目录下, 包含有 CONVENTIONS.org, DOCUMENTATION.org 等文档文件, 建议大家多多阅读.

第十三天: 定制你的 Layer

视频地址如下:

主要内容:

  1. 修复上一期视频中 occur-mode 启动的问题
  2. 修复 ivy0.8 导致的问题, 同时简单探讨了一下今后如何避免和处理类似的问题
  3. 介绍 post-init 和 pre-init 的用法, 介绍了如何定制 spacemacs 的 company-mode
  4. 介绍 layers.el 文件, 演示该文件的作用
  5. 介绍 layer 的 package 的 location 变量, 演示了如何从 github 获取并安装 package 的方法

修复上一期视频中的配置问题

在之前的配置代码中, 如果我们启动 emacs 会出现以下错误:

Symbol's function definition is void: evilified-state-evilify-map

这是因为这个符号在 config.el 中使用的时候还是空的, 我们可以通过以下方式修复, 编辑 config.el 文件, 将以下代码移动到 dotspacemacs/user-config 函数中:

(evilified-state-evilify-map occur-mode-map
  :mode occur-mode)

修复 ivy0.8 的问题

在 ivy 升级到0.8版本时, 对其中一个API的返回值进行了修改:

(let (res)
  (ivy-with
   '(ivy-read "test: "
	      '(("one" . 1) ("three" . 3))
	      :action (lambda (x) (setq res x)))
   "t C-m")
  res)
;; =>
;; ("three" . 3)

在之前的版本中, 这个函数的返回值是 3, 在0.8版本中被修改为了一个列表. 如果要修复这个问题, 我们需要在使用返回值的时候加上 cdr, 具体的修改可以查看fix break API changes for ivy 0.8.

post-init 和 pre-init

有一些 mode 已经安装, 例如 company-mode 已经被 auto-completion layer 安装, 如果这时我们还想对该 mode 进行一些定制, 那么我们可以这样处理:

  1. 在我们的 layer 中添加这个包
;; 添加 package
(defconst zilongshanren-packages
  '(youdao-dictionary
    company  ; 添加 company package
    )
  )

  1. 然后定义一个 post-init 函数
;; 定制 company-mode
(defun zilongshanren/post-init-company ()
  (setq company-minimum-prefix-length 1)
  )

然后重启 emacs 即可以看到定制的效果.

对于 package 的三个函数: pre-init, init, post-init, spacemacs是按照这个顺序来依次调用的.

location

在安装 package 时, 我们如果只输入 package 的名字, 那么默认是从 melpa 下载安装的. 如果我们想自定义 package 的安装地址, 那么我们就可以使用 location 变量.

自带 package

例如我们使用一个自带的 occur package:

;; 自定义 package 安装地址
(defconst zilongshanren-packages
  '(youdao-dictionary
    (occur-mode :location built-in)
    )
  )

;; 初始化 occur mode
(defun zilongshanren/init-occur-mode ()
  (evilified-state-evilify-map occur-mode-map
    :mode occur-mmode)
  )

从 github 安装

例如我们从 github 安装 gulpjs package:

;; 自定义 package 安装地址
(defconst zilongshanren-packages
  '(youdao-dictionary
    (occur-mode :location built-in)
    (gulpjs :location (recipe :fetcher github :repo "zilongshanren/emacs-gulpjs"))
    )
  )

(defun zilongshanren/init-gulpjs ()
  (use-package gulpjs
    :init)
  )

在 emacs 启动时就会从 github 上下载 guiljs package 并安装到本地.

layers.el

如果我们需要对某些 layer 中的 package 配置进行大量的重写, 那么我们可以移除这个 layer 的某个 package. 我们可以通过 layers.el 来实现这一点, 例如移除 chinese layer 的 youdao-dictionary package:

(configuration-layer/remove-layer 'youdao-dictionary)

然后我们可以在自己的 layer 中添加这个 package, 然后对它进行定制. 在这种情况下, spacemacs 不会在 chinese layer 中加载 youdao-dictionary 这个 package, 而是在我们的 layer 中加载这个 package, 以实现对 spacemacs 内置的package 的定制.

第十四天: 文件和 Buffer 操作

视频地址如下:

主要内容:

  1. 我的配置和 spacemacs配置的一些不同点
  2. 文件相关操作
  3. Buffer 相关操作
  4. Dired

不同点

从今天的视频开始, 将使用 子龙山人的配置 来讲解视频. 这份配置对 spacemacs 的定制有两个不同点:

  • 没有使用官方的 modeline, 而是采用自己定制的
  • 排除掉了大量的作者认为对他没有作用的 package, 因为这些 package 确实不经常使用, 反而可能导致一些 BUG 或者导致 spacemacs 启动或使用过程中变慢

文件相关操作

  1. SPC p f

    在当前的项目中查找文件, 类似于 vim 中的 Ctrl-p. 在作者的配置中, 该快捷键被绑定到了以下函数:

    (defun zilongshanren/open-file-with-projectile-or-counsel-git ()
      (interactive)
      (if (zilongshanren/vcs-project-root)
          (counsel-git)
        (if (projectile-project-p)
    	(projectile-find-file)
          (ido-find-file))))
    
    

    该函数会针对不同的项目类型使用不同的查找方式:

    • 如果是 git 项目, 那么使用 counsel-git 来查找文件, 不使用 projectile 的原因是 counsel-git 更快
    • 如果是 projectile 项目, 即在项目的根目录中存在 .projectile 文件, 那么使用 projectile-find-file 来查找文件
    • 否则使用 ido-fine-file 来查找文件
  2. SPC f f

    从当前目录开始查找文件. 在作者的配置中同时启用了 ivy-layer 和 helm-layer, 默认使用的是 helm 来查找文件.

  3. SPC f L

    使用 helm-locate 来在当前系统中查找文件.

  4. SPC f l

    查找文件并使用 literal 的方式来打开文件, 使用 literal 方式打开的文件不会附加编码信息, 例如 utf-8 编码中可能存在的 BOM 头信息, 使用 literal 模式即可以看到 BOM头.

  5. SPC f h

    查找文件并使用二进制的方式来打开文件, 可以使用 C-c C-c 回到之前的模式.

  6. SPC f o

    使用外部程序打开文件.

  7. SPC f E

    使用 sudo 来编辑文件, 当某些文件是只读的时候可以采用这种方式来编辑文件.

  8. SPC f D

    删除当前的文件和 buffer.

  9. SPC f j

    以当前文件的目录打开 dired buffer.

  10. SPC f r

    使用 ivy 打开最近文件列表.

  11. SPC f R

    重命名当前文件.

  12. SPC f v

    添加 local variables, 可以通过这个功能给项目做一些特殊的设置. 例如按下 SPC f v, 然后选择 add-dir-local-variable, 选择 org-mode, 再选择org-highlight-links 变量, 此时 emacs 会在当前文件的目录下生成一个 .dir-locals.el 文件, 内容如下:

    ;;; Directory Local Variables
    ;;; For more information see (info "(emacs) Directory Variables")
    
    ((org-mode
      (org-highlight-links)))
    
    

    这个文件中的代码会在当前目录下的所有文件 buffer 中生效.

  13. SPC f y

    拷贝当前文件的全路径.

  14. SPC f a d

    列出最近访问的目录, 使用命令行工具 fasd 实现.

  15. SPC f C d/u

    将当前文件的编码转换为 DOS/UNIX 编码.

  16. SPC f e d

    打开 .spacemacs 或 .spacemacs.d/init.el 文件.

  17. SPC f e i

    打开 .emacs 或 .emacs.d/init.el 文件.

  18. SPC f e l

    打开系统中已经安装的 el 文件.

  19. SPC f c

    复制文件.

  20. SPC f b

    打开标签.

  21. SPC f s/S

    保存当前 buffer 或 所有 buffer.

buffer 相关操作

  1. SPC b .

    打开 Buffer Selection Transient State, 在该模式下可以进行更多的操作, 由 hydra 提供.

  2. SPC b b

    切换到已经打开的 buffer.

  3. SPC b d

    关闭一个 buffer.

  4. SPC b f

    在 finder 中打开当前文件, 只在 Mac系统下生效.

  5. SPC b B/i

    以类似 Dired Mode 的形式打开 buffer 列表, 在这个列表中可以执行和 Dired Mode 类似的操作.

  6. SPC b h

    进入 \*spacemacs\* buffer.

  7. SPC b k

    使用正则表达式来删除 buffer.

  8. SPC b N

    新建一个 buffer.

  9. SPC b m

    删除除当前 buffer 外的所有 buffer.

  10. SPC b R

    使用 emacs 自动备份的文件恢复文件.

  11. SPC b s

    跳转到 scratch buffer.

  12. SPC b w

    关闭/打开 buffer 的 read-only.

  13. SPC b Y

    复制整个 buffer 的内容.

  14. SPC b P

    将剪切板的内容粘贴到整个 buffer.

  15. SPC <tab>

    在当前 buffer 和上一个打开的 buffer 中进行切换.

Dired

在第四天的内容中已经讲解过 Dired Mode 的操作, 具体可以查看 Dired Mode.

第十五天: layout, windows 和 project 相关

视频地址如下:

主要内容:

  1. Layout 相关操作
  2. Window 相关操作
  3. project 相关操作

Layout 相关操作

  1. SPC l L

    加载 layout 文件

  2. SPC l l

    在 layout 之间切换

  3. SPC l s

    将 layout 保存到文件

  4. SPC l <tab>

    在当前 layout 和上一个 layout 之间切换

  5. SPC l o

    配置 layout

  6. SPC l R

    重命名 layout

  7. SPC l ?

    显示更多的与 layout 相关的命令

Window 相关操作

  1. SPC w -

    上下拆分窗口

  2. SPC w /

    左右拆分窗口

  3. SPC w .

    显示更多的与 window micro state 的相关的命令

  4. SPC w 2/3

    左右显示 2/3 个窗口

  5. SPC w =

    将窗口均等分

  6. SPC w b

    切换到 minibuffer

  7. SPC w d

    删除当前窗口

  8. SPC w h/j/k/l

    向 左/下/上/右 移动窗口

  9. SPC w m

    最大化显示当前窗口

  10. SPC W H/J/K/L

    将当前窗口向 左/下/上/右 移动

  11. SPC w u/U

    取消/重置上次操作

  12. SPC w o

    切换到其他 frame

  13. SPC w F

    创建一个新的 frame

  14. SPC w 1/2/3/4

    切换到对应的编号的窗口

  15. SPC w w

    依次切换到其他窗口

  16. SPC w W

    使用字母标识需要跳转的窗口, 并按下字母进行跳转

  17. SPC t g

    将当前显示的窗口与其他窗口进行黄金分割显示

  18. SPC t -

    开启/关闭 将光标始终显示在中心行

project 相关操作

  1. SPC p f

    在当前 project 中查找并打开文件

  2. SPC p b

    在当前 project 中查找打开的 buffer

  3. SPC p p

    切换到其他的 project

  4. SPC p l

    切换到其他的 project 并创建一个新的 layout

  5. find-file-in-project

    这是一个插件, 支持全平台. 目前绑定在 SUPER f 快捷键上.

更多内容

对 Spacemacs 中的快捷键操作就介绍到这里, 更多的快捷键介绍可以到 Spacemacs ABC 去查看.

第十六天: 使用 ctags 和 company-etags

视频地址如下:

主要内容:

这期视频主要介绍 ctags 和 company-mode 的使用.

为什么使用 ctags

ctags 是一个开源的, 可以方便的对大型代码库进行索引的软件, 在使用 ctags 生成 tag 之后就可以非常方便的在这些 tag 中进行跳转. 因为有些编程语言, 例如 javascript 或者 lua, 它们不能进行精确的语义补全, 在有 ctags 进行索引补全的情况下也可以方便编写代码.

在之前也介绍过 term-mode, 也可以做到一些语义补全. 但是它也有一些缺点:

  1. 配置方式复杂, 对于比较大的项目的配置比较有难度
  2. 有些时候不能得到想要的补全结果

之前作者使用 YCMD 来对 C/C++ 代码进行补全, 但是它不太稳定, 现在已经切换为 ctags 来进行补全, 对于调试和 profile 会使用 IDE 进行操作.

如何配置 ctags

首先新建一个 testJs-ctags 目录, 然后在该目录下新建 a.js 以及 b.js 两个文件:

mkdir testJs-ctags
cd testJs-ctags
touch a.js
touch b.js

然后编辑 a.js 的内容如下:

var func1 = function () {
    console.log("func1");
};

var func2 = function () {
};

然后在 b.js 中的补全中可以显示处 func1 和 func2 的补全提示的. 为了更方便的讲解之后的内容, 我们可以查看使用的补全的后端: 输入 M-x, diminish-undo, 选择 company-mode, 这样在 modeline 就可以看到 company-mode 的具体信息.

再次输入 fun 等待弹出补全提示, 在补全选项中上下移动, 可以看到使用的补全后端包括 dabbrev-code 和 etags 等, 如果我们关闭 a.js 的 buffer, 就不会出现 func1 和 func2 的补全选项.

在之前的操作中, 我们并没有生成 ctags, 为什么也能使用 ctags 补全呢? 我们可以使用 SPC h d v, 然后输出 tags-table-list 来查看该变量的值, 当前的值是指向作者 cocos目录下的 TAGS 文件. 使用以下代码清空该值:

(setq-default tags-table-list nil)

然后再次尝试补全, 这时就不会使用 ctags 补全了.

那么如何生成 ctags 补全的文件呢? 使用以下命令即可:

cd testJs-ctags
ctags -e a.js
# 针对目录
# ctags -eR foldername

company-etags 在进行补全的时候, 会从变量 tags-table-list 值的文件列表中去查找 tags, 而且 tags 是不区分语言的.

如果需要手动加载 TAGS 文件, 那么可以调用 visit-tags-table 命令. 而在打开一个文件时, ctags 会从文件所在的目录进行查找, 一直到根目录, 加载所找到的 TAGS 文件.

如何高效的使用 ctags

自动重新生成 TAGS 文件

在使用 ctags 的过程中, 如果文件的内容被改变, 那么需要重新生成 TAGS 文件, 以便 ctags 的补全结果更精确. 作者实现了一个函数来自动加载必须的 TAGS 文件:

(defun my-setup-develop-environment ()
  (interactive)
  (when (my-project-name-contains-substring "guanghui")
    (cond
     ((my-project-name-contains-substring "cocos2d-x")
      ;; C++ project don't need html tags
      (setq tags-table-list (list (my-create-tags-if-needed "~/cocos2d-x/cocos"))))
     ((my-project-name-contains-substring "Github/fireball")
      (message "load tags for fireball engine repo...")
      ;; html project donot need C++ tags
      (setq tags-table-list (list (my-create-tags-if-needed "~/Github/fireball/engine/cocos2d")))))))

有另外一个工具函数, 当保存文件时会自动的重新生成 TAGS:

(defun my-auto-update-tags-when-save (prefix)
  (interactive "P")
  (cond
   ((not my-tags-updated-time)
    (setq my-tags-updated-time (current-time)))

   ((and (not prefix)
	 (< (- (float-time (current-time)) (float-time my-tags-updated-time)) 300))
    ;; < 300 seconds
    (message "no need to update the tags")
    )
   (t
    (setq my-tags-updated-time (current-time))
    (my-update-tags)
    (message "updated tags after %d seconds." (- (float-time (current-time)) (float-time my-tags-updated-time))))))

可以将 my-auto-udpate-tags-when-save 函数加入 after-save-hook 中, 或者绑定到快捷键上.

配置规则来生成更多的 TAGS

ctags 自身也有一个配置文件, 可以在该文件中定义规则来更好的生成 TAGS, 一个配置文件的示例如下:

--exclude=*.svn*
--exclude=*.git*
--exclude=*tmp*
--exclude=.#*
--tag-relative=yes
--recurse=yes

--langdef=js

--regex-js=/[ \t.]([A-Z][A-Z0-9._$]+)[ \t]*[=:][ \t]*([0-9"'\[\{]|null)/\1/n,constant/

--langdef=css
--langmap=css:.css
--regex-css=/^[ \t]*\.([A-Za-z0-9_-]+)/.\1/c,class,classes/

在配置文件中可以使用 –exclude 来忽略文件或路径, 使用 –langdef 来定义哪些文件属于 js 文件, 使用 –regex-js 来定义 TAGS 生成时的匹配规则. 这些匹配规则中可以使用正则表达式来提取内容生成 TAGS.

使用 etags-select 来浏览项目

在有 TAGS 之后, 可以使用 ctags 来方便的浏览文件内容. 例如在某个函数名上点击 [, g], 然后选择 etags-select-find-tag-at-point, 这时会把所有相关的内容列出到 buffer 中, 然后可以选择想要跳转的位置跳转过去.

最后的思考

company-etags 不能对所有的 mode 进行补全, 例如在 org-mode 中默认是不使用 company-etags 的. 因为在 company-etags.el 文件中有如下的代码:

(defvar company-etags-modes '(prog-mode c-mode objc-mode c++-mode java-mode
					jde-mode pascal-mode perl-mode python-mode))

在该变量中定义了可以使用 company-etags 的 mode, 将 org-mode 加入该变量的值中即可在 org-mode 使用 company-etags 进行补全.

ctags 支持上百种语言, 学会使用 ctags 能够提高效率.

Author: zilongshanren

Created: 2023-07-25 Tue 23:04

Validate