为 twidget 增加后端数据库

最近写了一个文本控件库 twidget,今天尝试用这个库在emacs中实现一个简单的笔记本应用,但我发现这种涉及大量文本数据且结构固定的应用更适合存储在数据库了。Twidget 虽然可以实现复杂多样的前端交互,但没有后端就像纸老虎。如果可以和数据库结合起来,便如虎添翼。于是便实现了 twidget-db 这个宏。

twidget-dbemacsqlemacsql-sqlite3 的使用做了一个封装。用户通过几行代码就可以建立数据库并进行增删改查操作。使用该宏后,可以得到两个函数 <prefix>-db<prefix>-db-action 。<prefix> 指 :prefix 属性的值。具体使用参考下面的例子。

(twidget-db "~/.emacs.d/notebook/notebook.db"
  :prefix "test"
  :tables '((notes [title content album timestamp])))

(test-db-action
 [:insert :into notes :values
          (["title" "content of note" "default" "2021-05-10"]
           ["title2" "content of note2" "default" "2021-05-11"])])
(test-db-action [:select [title content] :from notes])
;; => (("title" "content of note") ("title2" "content of note2"))

(test-db-action [:update notes :set [(= album "Emacs")
                                     (= content "new content of note")]
                         :where (= title "title2")])
(test-db-action [:select * :from notes])
;; => (("title" "content of note" "default" "2021-05-10") ("title2" "new content of note" "Emacs" "2021-05-11"))

(test-db-action [:delete :from notes])
(test-db-action [:select * :from notes])
;; => nil

上面的代码的意思是:在 ~/.emacs.d/notebook/notebook.db 文件中创建了数据库,并根据 :tables 参数的值生成了 notes 表。定义了 test-dbtest-db-action 两个函数。

test-db 返回一个数据库连接,可以被 emacsql 的函数使用,如 (emacsql (test-db) <sql>)test-db-action 做了进一步的封装,参数只有一个sql语句。Emacsql 的sql语句的写法请查看 emacsql文档

除了在 twidget 中使用,twidget-db 的代码也可以单独用于需要数据库的插件中。代码在 Github 自取。

发布于 2021-05-11

Twidget - Emacs中的文本控件库

Table of Contents

1 介绍

Twidget 的意思是 text widget, 即文本控件库。Emacs有一个 widget library,我使用后感觉不是很好用,并且需要使用鼠标操作。Twidget的设计遵循了简单易用,全键盘操作的理念,并且和 ewoc 做了很好的融合,可以轻松创建一个复杂的交互界面。

2 使用

2.1 基本控件

Twidget 目前有3个基本控件: twidget-text, twidget-choicetwidget-button 。我们可以使用一些统一的函数或宏来创建这些控件并插入到buffer中。每一种控件都有自己的键值对属性,可以通过指定键值对参数来控制其显示效果或行为。

twidget-text 文本控件

文本控件 有一个可编辑的区域,可以通过按键修改其展示的值并绑定到变量中。

主要键值对参数:

属性 类型 必须 含义
:bind 符号 控件的:value值会绑定到该符号变量中
:value 字符串 编辑区域的显示的值
:format 字符串 完整的控件字符串, 其中用 "[t]" 代表编辑区域
:action 函数/函数列表 编辑区域内容更新后触发的行为, 默认参数为当前的value
:plain 布尔值 隐藏编辑区域,显示为不可编辑的纯文本(和 twidget-insert 插入的文本不同的是,该区域可以通过代码修改)
:local 布尔值 是否为局部控件, 局部控件在每次刷新(twidget-page-refresh)时会恢复到原始值

twidget-choice 选择控件

选择控件 可以通过按键单选或多选一个列表中的值,并绑定到变量中。

主要键值对参数:

属性 类型 必须 含义
:bind 符号 控件的:value值会绑定到该符号变量中
:choices 列表 选项列表
:value 字符串/列表 选中的选项值,单选时是字符串,多选时是列表
:format 字符串 完整的控件字符串, 其中用 "[t]" 代表编辑区域
:action 函数/函数列表 选择选项后触发的行为, 默认参数为当前的value
:separator 字符串 不同选项间的分隔符,默认为一个空格
:multiple 布尔值 是否为多选, 默认为为单选
:fold 布尔值 是否折叠隐藏未选择的选项,默认全部显示
:require 布尔值 是否至少选中一项,默认非必须
:local 布尔值 是否为局部控件, 局部控件在每次刷新(twidget-page-refresh)时会恢复到原始值

twidget-button 按钮控件

按钮控件 点击后触发特定的行为。

属性 类型 必须 含义
:bind 符号 控件的:value值会绑定到该符号变量中
:value 字符串 按钮显示的文本
:action 函数 按钮作用后触发的行为
:help-echo 字符串 鼠标悬浮在按钮上时的tip,默认无
:follow-link 布尔值 按钮是否可用鼠标点击,默认不可

2.2 控件交互

使用 <tab> 激活下一个控件, <shift-tab> 激活上一个控件。被激活的控件会有数字提示,按数字键进行选择或文本更新,并触发相应的action函数。

2.3 控件API

环境准备

  • with-twidget-buffer (buffer-or-name &rest body)

    在指定 BUFFER-OR-NAME buffer 中创建控件。使用 pop-to-buffer 弹出buffer。

  • with-twidget-setup (&rest body)

    如果需要自己控制 buffer 的行为,使用该宏包裹twidget的代码。

  • twidget-buffer-setup & twidget-bind-keymap

    如果不使用 with-twidget-setup 宏,需要在 twidget 代码的开头和结尾分别调用上面的两个函数。

插入控件

  • twidget-create (twidget &rest args)

    twidget 是控件symbol,其余参数为键值对。

  • twidget-insert (&rest args)

    插入一段纯文本,和 insert 用法相同。

(defvar habit-regular-feq-type '("after-completion" "daily" "weekly" "monthly" "yearly"))
(defun habit-freq-type-switch (value)
  (message "current type is \"%s\"!" value))

(with-twidget-buffer "*Twidget Test*"
  (twidget-create 'twidget-text
    :bind 'habit-freq-title
    :value "Habit Frequency Selection"
    :format "Title: [t]"
    :action (lambda (value)
              (message "the title is \"%s\"" value)))
  (twidget-insert "\n\n")
  (twidget-create 'twidget-choice
    :bind 'habit-freq-type
    :choices habit-regular-feq-type
    :value "after-completion"
    :format "Repeat [t]"
    :action #'habit-freq-type-switch
    :separator "/"
    ;; :multiple nil
    ;; :fold nil
    ;; :local nil
    :require t))
example1.gif

查询控件属性值

  • twidget-query (bind-or-id property)

更新控件

一般用于 action 函数中

  • twidget-update (bind-or-id &rest properties)

    更新单个控件。bind-or-id 指被更新的控件的 :bind 属性值或 twidget-id(仅开发用)。properties 是一系列需要更新的键值对。

(defvar example-editors '("emacs" "vim" "vscode" "sublime text"))
(with-twidget-buffer "*Twidget Test*"
  (twidget-create 'twidget-choice
    :bind 'example-editor
    :choices example-editors
    :format "Editors: [t]"
    :value "emacs"
    :separator "/"
    :action (lambda (value)
              (twidget-update
               'example-string :value (capitalize value)))
    :require t)
  (twidget-insert "\n\n")
  (twidget-create 'twidget-text
    :bind 'example-string
    :format "  - [t] is my favorite editor!"
    :value "Emacs"
    :plain t)
  (twidget-create 'twidget-button
    :value "switch"
    :action (lambda (btn)
              (let* ((choices example-editors)
                     (editor (downcase example-editor))
                     (nth (seq-position choices editor)))
                (twidget-update
                 'example-editor
                 :value (capitalize (nth (% (1+ nth) (length choices)) choices)))))))
example2.gif
  • twidget-multi-update (&rest twidget-properties)

    更新多个控件。twidget-properties 的形式参考例子。

(defvar example-editors '("emacs" "vim" "vscode" "sublime text"))
(defvar example-websites
  '(("emacs" "https://www.gnu.org/software/emacs/")
    ("vim" "https://www.vim.org")
    ("vscode" "https://code.visualstudio.com")
    ("sublime text" "https://www.sublimetext.com")))

(with-twidget-buffer "*Twidget Test*"
  (twidget-create 'twidget-choice
    :bind 'example-editor
    :choices example-editors
    :format "\nEditors: [t]"
    :value "emacs"
    :separator "/"
    :action (lambda (value)
              (twidget-multi-update
               'example-string `(:value ,(capitalize value))
               'example-link `(:value ,(assoc value example-websites))))
    :require t)
  (twidget-create 'twidget-button
    :value "#switch#"
    :action (lambda (btn)
              (let* ((choices example-editors)
                     (editor (downcase example-editor))
                     (nth (seq-position choices editor)))
                (twidget-update
                 'example-editor
                 :value (nth (% (1+ nth) (length choices)) choices)))))
  (twidget-insert "\n\n")
  (twidget-create 'twidget-text
    :bind 'example-string
    :format "  - [t] is my favorite editor."
    :value "Emacs"
    :plain t)
  (twidget-create 'twidget-text
    :bind 'example-link
    :format "\n  - The website of [t0] is [t1]."
    :value '("emacs" "https://www.gnu.org/software/emacs/")
    :plain t))
example3.gif

删除控件

一般用于 action 函数中

  • twidget-delete (&rest binds-or-ids)

    bind 指控件绑定的变量,id 指 overlay twidget-id 的值。

(with-twidget-buffer "*Twidget Test*"
  (twidget-create 'twidget-choice
    :bind 'example-num
    :choices '("1" "2" "3" "4")
    :format "\nDelete the number [t] item in list."
    :value '("1") :separator "/"
    :multiple t)
  (twidget-create 'twidget-button
    :value "Delete"
    :follow-link t
    :action (lambda (btn)
              (let* ((binds (mapcar (lambda (num)
                                      (intern (format "example-str%s" num)))
                                    example-num))
                     (choices (twidget-query 'example-num :choices))
                     (new-choices
                      (seq-remove (lambda (num) (member num example-num)) choices)))
                (apply #'twidget-delete binds)
                (twidget-update 'example-num
                                ;; if update :choice, :value should also be updated.
                                :value (car new-choices)
                                :choices new-choices))))
  (twidget-insert "\n")
  (twidget-create 'twidget-text
    :bind 'example-str1
    :value "\n  1. this is the 1st item."
    :plain t)
  (twidget-create 'twidget-text
    :bind 'example-str2
    :value "\n  2. this is the 2nd item."
    :plain t)
  (twidget-create 'twidget-text
    :bind 'example-str3
    :value "\n  3. this is the 3rd item."
    :plain t)
  (twidget-create 'twidget-text
    :bind 'example-str4
    :value "\n  4. this is the 4th item."
    :plain t))
example4.gif

2.4 控件组API

控件组是由多个控件组合而成的。定义控件组可以复用相同的结构的控件,只更新需要更新的控件组,这对实现复杂的交互很有帮助。

  • twidget-group (&rest body) 定义控件组
  • twidget-group-create (group &optional next-group) 创建控件组
  • twidget-group-delete (group) 删除控件组
  • twidget-page-create (&rest groups) 创建所有控件组
  • twidget-page-refresh (&rest groups) 更新所有控件组
(twidget-group 'example-header
  (twidget-create 'twidget-choice
    :bind 'example-tab
    :choices '("主页" "关于" "记录" "更多")
    :format (concat (propertize "戈楷旎" 'face '(bold :height 1.2)) "   [t]")
    :value "主页" :separator "  "
    :action 'example-switch-tabs
    :require t))

(defun example-switch-tabs (value)
  (pcase value
    ("主页" (twidget-page-refresh 'example-header
                                  'example-index 'example-footer))
    ("关于" (twidget-page-refresh 'example-header
                                  'example-about 'example-footer))
    ("记录" (twidget-page-refresh 'example-header
                                  'example-diary 'example-footer))
    ("更多" (twidget-page-refresh 'example-header
                                  'example-more 'example-footer))))

(twidget-group 'example-index
  (twidget-insert "\n\n")
  (twidget-insert "This is the content of index page."))

(twidget-group 'example-about
  (twidget-insert "\n\n")
  (twidget-insert "This is the content of about page."))

(twidget-group 'example-diary
  (twidget-insert "\n\n")
  (twidget-insert "This is the content of diary page."))

(twidget-group 'example-more
  (twidget-create 'twidget-choice
    :bind 'example-more-tabs
    :choices '("留言" "视频")
    :format "> [t]"
    :action 'example-switch-more-tabs
    :require t))

(defun example-switch-more-tabs (value)
  (pcase value
    ("留言" (twidget-page-refresh 'example-header 'example-more
                                  'example-message 'example-footer))
    ("视频" (twidget-page-refresh 'example-header 'example-more
                                  'example-video 'example-footer))))

(twidget-group 'example-message
  (twidget-insert "\n\n")
  (twidget-insert "This is the content of message page."))

(twidget-group 'example-video
  (twidget-insert "\n\n")
  (twidget-insert "This is the content of video page."))

(twidget-group 'example-footer
  (twidget-insert "\n\n2019-" (format-time-string "%Y")
                  " 戈楷旎 | Licensed under CC BY-NC-SA | Powered by Django"))

(with-twidget-buffer "*twidget test*"
  (twidget-page-create 'example-header 'example-index 'example-footer))
example5.gif

3 说明

Twidget 目前处于测试开发中,后续 API 可能会有变动。如果使用,请密切关注更新。

发布于 2021-05-10

Emacs Hack - "Org宏替代" 生成文章辅助信息

1 问题描述

一篇博客文章除了标题、正文,还有一些辅助信息,比如:日期,分类,字数等。使用原生 org-mode 生成的 html 不会自动生成这些内容。于是想hack一下,折腾后效果例如: [ 分类: 博客 / 日期: 2020-02-21 / 字数: 1019 ]

2 Org宏替代

这里主要使用了orgMode的 Macro Replacement 功能。我们可以在org文件开头定义宏,然后在正文需要的位置调用宏就可以实现辅助信息的插入。

例如,定义如下宏:

#+MACRO: poem Rose is $1, violet's $2. Life's ordered: Org assists you.

其中,"poem" 为宏的名称,后面的内容为宏的内容,"$1,$2" 为参数。然后在合适的位置调用宏:

{{{poem(red,blue)}}}

最终的结果是在调用宏的位置插入文本 "Rose is red, violet's blue. Life's ordered: Org assists you."。相信这种功能也不难理解,类似于函数的定义与调用。

除了插入一串字符,预先在org文件开头定义的一些 keyword 可以直接调用。也可以在宏中调用elisp函数,实现更为复杂的功能。辅助信息的生成就依赖这两个功能。

3 具体实现

为了实现上面的效果,我在文章开头插入了如下的宏调用:

#+begin_center
[ 分类: {{{keyword(category)}}} / 日期: {{{date}}} / 字数: {{{wc}}} ]
#+end_center

其中 {{{date}}} 直接调用了 #+DATE: 预定义的日期, {{{keyword(category)}}} 调用了 #+CATEGORY: 预定义的分类。这里要说明的是,对于 org-mode 已有的 keyword,如 "TITLE, AUTHOR, DATE" 这些可以直接调用。自定义的 keyword 如 "CATEGORY" 需要定义为形如 {{{keyword(category)}}} 的格式。最后,文章的字数需要使用一个 elisp 函数统计,这里需要定义一个统计字数的宏:

#+MACRO: wc (eval (gk-word-count))

不难理解,这个宏执行了 gk-word-count 这个函数,然后将返回值插入到“字数: ”后面。这个统计字数的函数定义在这里

OK,这样每次在org发布项目的时候就会自动添加日期、分类和统计字数了。但是,每次在写文章的时候都要在开头加上 这一段内容 实在是麻烦。可以将这些内容放在单独的文件中,然后用 #+INCLUDE: 引入。

我将它们放在 post-info.org 这个文件中,完整的内容是:

#+MACRO: wc (eval (gk-word-count))
#+begin_center
[ 分类: {{{keyword(category)}}} / 日期: {{{date}}} / 字数: {{{wc}}} ]
#+end_center

然后在文章开头加上 #+INCLUDE: "<RELATIVE/DIRECTORY/TO>/post-info.org" 即可。

4 使用注意

如果宏定义中执行的函数需要参数,参数必须是字符串类型。比如参数中传入一个list,这个list就变成了字符串。在函数执行前需要先 (read <list>) 将字符串转为列表。

发布于 2020-09-10

Emacs Lisp - ewoc使用介绍

1 介绍

Ewoc 是 Emacs's Widget for Object Collections的简写,它可以根据lisp对象的结构绘制和更新buffer的文本,就像MVC设计模式中的视图层。其中,生成的buffer的文本分为三个部分:特定的头部文本,lisp对象代表的数据元素的文本,特定的底部文本。一个ewoc对象的信息包含在以下内容中:

  • 用于产生文本的buffer
  • buffer中文本的起始位置
  • 头部和底部文本的内容
  • 一个双向链接的结点链,每一个结点包含:
    • 一个数据元素(单个lisp对象)
    • 结点链中上一个和下一个结点的链接
  • 一个将数据元素的文本插入到buffer中的打印函数

使用 ewoc-create 来定义一个ewoc对象,然后用其他的ewoc函数构建结点的结构并在buffer中显示。一旦在buffer中显示了文本,便可使用其他函数负责buffer的光标位置和结点间的数据通信。

让结点包含数据元素就像给变量设置值。通常结点包含数据元素的行为发生在将结点加入到ewoc对象过程中。可以通过下面两个函数获取和设置数据元素的值:

(ewoc-data node)
⇒ value

(ewoc-set-data node new-value)
⇒ new-value

也可以使用lisp对象(list or vector)或其他结构的索引作为数据元素的值。

当数据改变时,buffer中文本会相应的更新。使用 ewoc-update 函数更新所有结点,或者用 ewoc-invalidate ,使用 ewoc-map 函数给所有的结点增加条件判断。同时,可以使用 ewoc-deleteewoc-filter 函数删除无效的结点并设置新的结点。删除一个结点的同时会删掉该结点在buffer中展示的文本。

2 函数

这一部分的术语中, ewocnode 分别代表前面提到的两种结构,而 data 代表作为数据元素的lisp对象。三者的关系是:ewoc包含node,node包含data。下面详细介绍ewoc的每个函数。

ewoc-create pretty-printer &optional header footer nosep

创建并返回一个不包含结点的ewoc对象。 pretty-printer 是包含一个数据元素参数的函数,该数据元素的文本值将被插入到buffer中。(使用 insert 函数插入,不要用 insert-before-markers ,因为它对ewoc包的内部机制有干扰)

通常,该函数会自动在头部文本,底部文本和每一个node的文本后插入新的一行。设置 nosep 参数为 non-nil 可以不插入新行,这在需要将整个ewoc的文本显示在一行时很有用。ewoc创建时会在当前buffer下维护文本,所以在调用创建函数前要先切换到目标buffer。

ewoc-buffer ewoc

返回维护ewoc文本的buffer。

ewoc-get-hf ewoc

返回一个由头部文本和底部文本构成的 cons cell (header . footer)。

ewoc-enter-first ewoc data
ewoc-enter-last ewoc data

分别在ewoc的结点链的开头和结尾添加包含数据元素的新结点。

ewoc-enter-before ewoc node data
ewoc-enter-after ewoc node data

分别在指定结点的前面和后面添加一个包含数据元素的新结点。

ewoc-prev ewoc node
ewoc-next ewoc node

分别返回指定结点的前一个和后一个结点。

ewoc-nth ewoc n

返回以0开始的索引n的结点,n为负值时从最后开始索引。n超出范围时返回nil。

ewoc-data node

获取并返回node包含的数据元素。

ewoc-set-data node

设置node包含的数据元素值为data。

ewoc-locate ewoc &optional pos guess

确定并返回包含光标位置的结点。如果ewoc没有结点,返回nil。如果 pos 在第一个结点之前,返回第一个结点;在最后一个结点之后,返回最后一个结点。可选参数 guess 是有可能在pos附近的结点,它不会改变函数的结果但会让函数执行的更快。

ewoc-location node

返回结点的起始位置。

ewoc-goto-prev ewoc arg
ewoc-goto-next ewoc arg

分别移动光标到上arg个或下arg个结点。如果已经在第一个结点或ewoc为空, ewoc-goto-prev 不移动光标并返回nil,同样 ewoc-goto-next 不超过最后一个结点。除了以上特殊情况,函数返回移动到的结点。

ewoc-goto-node ewoc node

移动光标到结点的起始位置。

ewoc-refresh ewoc

重新生成ewoc的文本。执行过程是:删除header和footer之间的文本,然后为每一个结点依次调用 pretty-printer 函数生成文本。

ewoc-invalidate ewoc &rest nodes

ewoc-refresh 类似,但只更新 nodes 列表内的结点而不是所有结点。

ewoc-delete ewoc &rest nodes

删除所有 nodes 列表内的结点。

ewoc-filter ewoc predicate &rest args

为ewoc中的每一个数据元素调用 predicate 函数,删除断言为nil的结点。 args 是传递给断言函数的参数。

ewoc-collect ewoc predicate &rest args

为ewoc中的每一个数据元素调用 predicate 函数,返回断言为non-nil的元素列表。元素在列表中的顺序和buffer中一致。 args 是传递给断言函数的参数。

ewoc-map map-function ewoc &rest args

为ewoc中的每一个数据元素调用 map-function , 更新map函数返回为non-nil的结点。 args 是传递给map函数的参数。

3 例子

下面是使用ewoc实现的显示颜色组成的例子,颜色组成由buffer中的三个整数组成的向量表示。

(setq colorcomp-ewoc nil
      colorcomp-data nil
      colorcomp-mode-map nil
      colorcomp-labels ["Red" "Green" "Blue"])

(defun colorcomp-pp (data)
  (if data
      (let ((comp (aref colorcomp-data data)))
        (insert (aref colorcomp-labels data) "\t: #x"
                (format "%02X" comp) " "
                (make-string (ash comp -2) ?#) "\n"))
    (let ((cstr (format "#%02X%02X%02X"
                        (aref colorcomp-data 0)
                        (aref colorcomp-data 1)
                        (aref colorcomp-data 2)))
          (samp " (sample text) "))
      (insert "Color\t: "
              (propertize samp 'face
                          `(foreground-color . ,cstr))
              (propertize samp 'face
                          `(background-color . ,cstr))
              "\n"))))

(defun colorcomp (color)
  "Allow fiddling with COLOR in a new buffer.
     The buffer is in Color Components mode."
  (interactive "sColor (name or #RGB or #RRGGBB): ")
  (when (string= "" color)
    (setq color "green"))
  (unless (color-values color)
    (error "No such color: %S" color))
  (switch-to-buffer
   (generate-new-buffer (format "originally: %s" color)))
  (kill-all-local-variables)
  (setq major-mode 'colorcomp-mode
        mode-name "Color Components")
  (use-local-map colorcomp-mode-map)
  (erase-buffer)
  (buffer-disable-undo)
  (let ((data (apply 'vector (mapcar (lambda (n) (ash n -8))
                                     (color-values color))))
        (ewoc (ewoc-create 'colorcomp-pp
                           "\nColor Components\n\n"
                           (substitute-command-keys
                            "\n\\{colorcomp-mode-map}"))))
    (set (make-local-variable 'colorcomp-data) data)
    (set (make-local-variable 'colorcomp-ewoc) ewoc)
    (ewoc-enter-last ewoc 0)
    (ewoc-enter-last ewoc 1)
    (ewoc-enter-last ewoc 2)
    (ewoc-enter-last ewoc nil)))

通过定义改变 colorcomp-data 的值,完成选择过程和按键绑定,这个例子可以拓展成一个颜色选择的组件(MVC模式中的模型)。

(defun colorcomp-mod (index limit delta)
  (let ((cur (aref colorcomp-data index)))
    (unless (= limit cur)
      (aset colorcomp-data index (+ cur delta)))
    (ewoc-invalidate
     colorcomp-ewoc
     (ewoc-nth colorcomp-ewoc index)
     (ewoc-nth colorcomp-ewoc -1))))

(defun colorcomp-R-more () (interactive) (colorcomp-mod 0 255 1))
(defun colorcomp-G-more () (interactive) (colorcomp-mod 1 255 1))
(defun colorcomp-B-more () (interactive) (colorcomp-mod 2 255 1))
(defun colorcomp-R-less () (interactive) (colorcomp-mod 0 0 -1))
(defun colorcomp-G-less () (interactive) (colorcomp-mod 1 0 -1))
(defun colorcomp-B-less () (interactive) (colorcomp-mod 2 0 -1))

(defun colorcomp-copy-as-kill-and-exit ()
  "Copy the color components into the kill ring and kill the buffer.
     The string is formatted #RRGGBB (hash followed by six hex digits)."
  (interactive)
  (kill-new (format "#%02X%02X%02X"
                    (aref colorcomp-data 0)
                    (aref colorcomp-data 1)
                    (aref colorcomp-data 2)))
  (kill-buffer nil))

(setq colorcomp-mode-map
      (let ((m (make-sparse-keymap)))
        (suppress-keymap m)
        (define-key m "i" 'colorcomp-R-less)
        (define-key m "o" 'colorcomp-R-more)
        (define-key m "k" 'colorcomp-G-less)
        (define-key m "l" 'colorcomp-G-more)
        (define-key m "," 'colorcomp-B-less)
        (define-key m "." 'colorcomp-B-more)
        (define-key m " " 'colorcomp-copy-as-kill-and-exit)
        m))
发布于 2020-08-27

Emacs Hack - 通过列表数据创建表格

1 问题描述

OrgMode内置的创建表格的函数是 org-table-create , 传入"列数x行数"参数即可生成特定行数、列数的表格。这种交互函数在编写org文档时很实用,但在代码中却显得鸡肋。因为在代码中,我们通常希望表格和数据可以一起生成,而不是手动添加数据。

我在折腾 gk-habit.el 时,就产生了这样的需求:生成习惯的月度打卡视图。就像下面这个样子:

Sun Mon Tue Wed Thu Fri Sat
            1
            --
2 3 4 5 6 7 8
-- -- -- -- -- --
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31          
         

2 思路分析

解决这个问题的关键是把握几个操作org表格的函数 org-create-table, org-table-next-field, org-table-insert-hline, org-table-kill-row… 然后就是在表格创建的过程中依次插入数据。用于创建表格的每一行的数据用列表表示,分隔线用 hl 对象表示。

  • 首先创建一个一行n列的表格,因为 org table 的函数只有在表格内才能使用。其中n为每行元素的个数。
  • 在数据列表中循环,如果元素是一个list,表示是数据。继续在该list中循环,插入数据后跳到下一个单元格(注意数字要转为字符串)。
  • 如果元素是 hl 对象,表示是分隔线,直接插入一行分割线 (org-table-insert-hline 1)
  • 每一行插入最后一个数据后会执行“跳到下一个单元格”的操作,当右边没有单元格时会自动插入新的一行。
  • 因此,最后会多出一行,用 org-table-kill-row 函数删掉。

3 代码实现

(defun gk-org-table-create (LIST)
  "Create org table from a LIST form at point."
  (let ((column (catch 'break
                  (dolist (row-data LIST)
                    (when (listp row-data)
                      (throw 'break (length row-data))))))
        (beg (point)))
    (org-table-create (concat (number-to-string column) "x1"))
    (goto-char beg)
    (when (org-at-table-p)
      (org-table-next-field)
      (dotimes (i (length LIST))
        (let ((row-data (nth i LIST)))
          (if (listp row-data)
              (dolist (data row-data)
                (cond
                 ((numberp data)
                  (insert (number-to-string data)))
                 ((null data)
                  (insert ""))
                 (t (insert data)))
                (org-table-next-field))
            (when (equal 'hl row-data)
              (org-table-insert-hline 1)))
          (when (= i (1- (length LIST)))
            (org-table-kill-row))))))
  (forward-line))

4 使用案例

(gk-org-table-create
 '(("n1" "n2" "n3" "n4" "n5")
   hl
   (1 2 3 4 5)
   (6 7 8 9 10)
   hl
   ("c1" "c2" "c3" "c4" "c5")
   hl
   ("a" "b" "c" "d" "e")
   ("f" "g" "h" "i" "j")))
| n1 | n2 | n3 | n4 | n5 |
|----+----+----+----+----|
| 1  | 2  | 3  | 4  | 5  |
| 6  | 7  | 8  | 9  | 10 |
|----+----+----+----+----|
| c1 | c2 | c3 | c4 | c5 |
|----+----+----+----+----|
| a  | b  | c  | d  | e  |
| f  | g  | h  | i  | j  |

实现开篇提出的习惯打卡的视图是个更复杂的问题,这里涉及到了不同月份的天数不同,起始星期不同,以及每天对应的打卡状态不同等问题。解决了这些问题后,将得到的数据整合成 gk-org-table-create 合法的数据列表形式即可生成相应的表格。相关代码在这里

如果你有更简单、漂亮的实现,欢迎留言探讨~

发布于 2020-08-19

Emacs Hack - 习惯记录与打卡的demo

想要培养一些好的习惯,想要不那么费力的变得更自律。

读了一些关于时间管理和习惯培养的书籍和文章,我意识到“仪式感”和“记录反思”的重要性。事实上,以自己喜欢的方式做记录是件很开心的事情,我已经连续80天写晨间日记和晚间总结。现在, 写日志已经变成了习惯。但我希望培养更多好的习惯,用我自己喜欢的方式(在emacs中),所以有了gk-habit这个demo。

2 思路

gk-habit使用emacsql-sqlite创建数据库,有两个表 "habit" 和 "record"。前者记录习惯及其相关参数,后者记录习惯打卡的数据。

habit表

  • create-time 习惯创建的时间
  • name 习惯名称
  • frequency-type 习惯打卡频率类型,包括“每天”、“每周几重复”,“一周几次”,“一月几次”
  • frequency-param 频率类型的参数,每天重复则为nil、每周几重复则用数字表示(134表示每周一三四)、一周或一月的打卡的次数。
  • period 习惯发生的时间时间段
  • remind-time 提醒打卡的时间
  • remind-string 提醒打卡时的文本
  • status 习惯的状态,active或archive,archive表示不再跟踪该习惯
  • archive-time 归档的时间

record表

  • create-time 记录创建的时间
  • habit 记录的习惯
  • status 习惯完成状态(DONE完成,MISS未完成)
  • comment 未完成的原因

3 代码

;;; habit record - gk-habit
(require 'cl-lib)
(require 'emacsql-sqlite)

(defconst gkh-buffer "*Gk-Habit*")

(defvar gkh-db (emacsql-sqlite3 "~/.emacs.d/gk-habit/habit.db"))

(defvar gkh-file "~/.emacs.d/gk-habit/habit.org")

(defvar gkh-record-status '("DONE" "MISS"))

(defvar gkh-frequency-type '("everyday" "repeat" "by-week" "by-month"))

(defvar gkh-period '("get-up" "morning" "noon" "afternoon" "evening" "before-sleep"))

(defvar gkh-table-habit-column
  '("create-time" "name" "frequency" "frequency-param"
    "peroid" "remind-time" "remind-string" "status" "archive-time"))

(defvar gkh-table-record-column
  '("create-time" "habit" "status" "comment"))

(defun gkh-db-create-tables ()
  (interactive)
  "Create database tables of gk-habit."
  (emacsql gkh-db [:create-table habit
                                 ([(create-time string :primary-key :not-null)
                                   (name string :not-null :unique)
                                   (frequency-type string :not-null)
                                   (frequency-param)
                                   (period string :not-null)
                                   (remind-time)
                                   (remind-string)
                                   (status string :not-null)
                                   (archive-time string)])])

  (emacsql gkh-db [:create-table record
                                 ([(create-time string :primary-key :not-null)
                                   (habit string :not-null)
                                   (status string :not-null)
                                   (comment string)]
                                  (:foreign-key [habit] :references habit [name]
                                                :on-delete :cascade))]))

(defun gkh-db-drop-tables ()
  "Drop database tables of gk-habit."
  (interactive)
  (let* ((tables (emacsql gkh-db [:select name
                                          :from sqlite_master
                                          :where (= type 'table)]))
         (table (completing-read "Choose a table: " tables nil t)))
    (emacsql gkh-db `[:drop-table ,(intern table)])))

(defun gkh--frequency-params (frequency-type)
  "Get the habit frequency parameters"
  (let (param)
    (cond
     ((string= frequency-type "everyday")
      (setq param nil))
     ((string= frequency-type "repeat")
      (setq param (completing-read "repeated day (exam. \"135\" means habit repeat on Monday, Wensday and Friday in every week.): " nil)))
     ((string= frequency-type "by-week")
      (setq param (completing-read "how many times a week: " nil)))
     ((string= frequency-type "by-month")
      (setq param (completing-read "how many times a month: " nil))))
    param))

(defun gkh-init ()
  "Gk-habit initialize, create database and org tables."
  (interactive)
  (ignore-errors (gkh-db-create-tables))
  (gkh-org-table-draw))

(defun gkh-new ()
  "Add a new habit"
  (interactive)
  (cl-block 'return
    (let* ((create-time (format-time-string "%Y-%m-%d %T"))
           (habit
            (let ((temp (completing-read "name of habit: " nil))
                  (habits (mapcar 'car (emacsql gkh-db [:select name :from habit
                                                                :where (= status "Active")]))))
              (if (member temp habits)
                  (cl-return-from 'return
                    (message "the habit '%s' already exist!" temp))
                temp)))
           (frequency-type (completing-read "frequency of habit: " gkh-frequency-type nil t))
           (frequency-param (gkh--frequency-params frequency-type))
           (period  (completing-read "period of habit: " gkh-period nil t))
           (remind-time
            (let ((temp (completing-read "remind this habit at: " nil)))
              (if (string= "" temp)
                  nil temp)))
           (remind-string
            (let ((temp (completing-read "habit remind sentence: " nil)))
              (if (string= "" temp)
                  nil temp))))
      (emacsql gkh-db `[:insert :into habit
                                :values ([,create-time ,habit ,frequency-type ,frequency-param ,period ,remind-time ,remind-string "Active" nil])])
      (gkh-org-table-draw)
      (message "Habit '%s' is added!" habit))))

(defun gkh-record ()
  "Insert a habit redord in table."
  (interactive)
  (let* ((create-time (format-time-string "%Y-%m-%d %T"))
         (habit (completing-read "Choose a habit: "
                                 (emacsql gkh-db [:select [name] :from habit
                                                          :where (= status "Active")])))
         (status (completing-read "Is the habit done?" gkh-record-status nil t))
         (comment
          (when (string= "MISS" status)
            (completing-read "Reason why missed: " nil))))
    (emacsql gkh-db `[:insert-into record
                                   :values ([,create-time ,habit ,status ,comment])])
    (gkh-org-table-draw)
    (message "Habit '%s' is %s, record on %s, %s" habit status create-time comment)))

(defun gkh-archive ()
  "Archive a habit"
  (interactive)
  (let* ((habits (emacsql gkh-db [:select name :from habit
                                          :where (= status "Active")]))
         (habit (completing-read "Choose a habit: " habits nil t)))
    (emacsql gkh-db `[:update habit
                              :set [(= status "Archive") (= archive-time ,(format-time-string "%Y-%m-%d %T"))]
                              :where (= name ,habit)])
    (gkh-org-table-draw)
    (message "habit %s has been archived!" habit)))

(defun gkh-org-table-draw ()
  "Draw gk-habit database in org table."
  (interactive)
  (let* ((table-alist '(("habit" . gkh-table-habit-column)
                        ("record" . gkh-table-record-column))))
    (with-temp-file gkh-file
      (goto-char (point-min))
      (dotimes (i (length table-alist))
        (let* ((headline (car (nth i table-alist)))
               (column-list (eval (cdr (nth i table-alist))))
               (column-num (length column-list)))
          (insert (concat "* " headline " table\n"))
          (org-table-create (concat (format "%s" column-num) "x2"))
          (dotimes (j column-num)
            (org-table-next-field)
            (insert (nth j column-list)))
          (let ((items (emacsql gkh-db `[:select * :from ,(intern headline)])))
            (dotimes (m (length items))
              (dotimes (n column-num)
                (org-table-next-field)
                (insert (format "%s" (nth n (nth m items)))))))
          (org-table-align)
          (forward-line 2)
          (end-of-line)
          (newline 2))))))

(defun gkh-org-table-display ()
  "Display gk-habit org table in a bottom buffer."
  (interactive)
  (gkh-org-table-draw)
  (if (string= (buffer-name) gkh-buffer)
      (message "Already in the Gk Habit buffer."))
  (select-window
   (or (get-buffer-window gkh-buffer)
       (selected-window)))
  (with-current-buffer (get-buffer-create gkh-buffer)
    (org-mode)
    (read-only-mode -1)
    (erase-buffer)
    (insert (file-contents gkh-file))
    (valign-mode)
    (goto-char (point-min))
    (read-only-mode 1))
  (view-buffer gkh-buffer 'kill-buffer))

(provide 'gk-habit)

4 计划

  • 设置习惯打卡时间提醒
  • 使用matplotlib库绘制习惯打卡统计图
发布于 2020-08-04

Emacs Workflow - 文件管理

Geekinney 说: "文件管理"是每个操作系统最基本的功能。当我们使用计算机时,不可避免的要和文件打交道,比如:文件的创建、重命名、拷贝、移动、删除、搜索… 通常,我们通过鼠标来完成这些操作。但你有没有想过,这种鼠标点击和拖动的操作实际是非常低效的,我们在鼠标上浪费了大量的时间!

此刻,Linux用户加入了会话:“我们不用鼠标,我们用命令行。” 诚然,Linux命令行确实比鼠标高效很多。但你有没有想过存在一种更加便捷的方式, 结合了GUI的直观和TUI的高效,将每种命令换成一个或简单的几个按键来完成 ?这就是我要介绍的emacs文件管理系统。

在阅读下文之前,请先在 .emacs.d 目录下的 elisp 文件夹中创建 init-filemanage.el 文件, 我们将用这个文件来存储所有与文件管理相关的配置。

1 基础

1.1 文件目录结构

Emacs中浏览文件目录结构主要使用内置的 dired-mode 及其拓展 dired-x

由于是内置的package,无需额外安装。在emacs中直接通过按键 C-x d (调用 dired 命令)后输入要打开的目录后回车,即可打开对应的目录结构。默认情况下,emacs将当前buffer的所在的目录作为打开的目录。在改变目录路径时可以使用 TAB 键补全。

或者,通过按键 C-x C-j (调用 dired-jump 命令,这是dired-x的功能)直接打开当前buffer文件所在的目录。dired目录结构如下图:

dired-mode.png

1.1.1 文件导航

打开目录结构后,使用 n/pC-n/C-p 或方向键都可以在文件和目录间移动。在目录上按回车键会打开新的目录,在文件上按回车键会打开该文件。在 .. 上按回车键可以跳转到上级目录。除了这种方式,还可以按 C-x C-j 回到上级目录。其他的按键操作:

  • SPC: 向下移动一行
  • ^: 跳转到上一目录级
  • <: 移到上一个目录行,跳过文件行
  • >: 移到下一个目录行,跳过文件行
  • j: 移到指定文件行

1.1.2 文件浏览

  • A: 按照正则搜索文件,列出搜索结果
  • v: 以view-mode(只读)浏览文件内容
  • o: 在另一个窗口打开文件或目录
  • i: 在当前窗口插入子目录
  • s: 对列表按名字或日期排序

一些文件在emacs中的浏览体验不是很好,可以使用系统默认程序浏览文件,将下面这段代码(来自xah-emacs)加入 init-filemanage.el 文件中:

(defun xah-open-in-external-app (&optional @fname)
  "Open the current file or dired marked files in external app.
The app is chosen from your OS's preference.
When called in emacs lisp, if @fname is given, open that.
URL `http://ergoemacs.org/emacs/emacs_dired_open_file_in_ext_apps.html'
Version 2019-11-04"
  (interactive)
  (let* (($file-list
          (if @fname
              (progn (list @fname))
            (if (string-equal major-mode "dired-mode")
                (dired-get-marked-files)
              (list (buffer-file-name)))))
         ($do-it-p (if (<= (length $file-list) 5)
                       t
                     (y-or-n-p "Open more than 5 files? "))))
    (when $do-it-p
      (cond
       ((string-equal system-type "windows-nt")
        (mapc
         (lambda ($fpath)
           (w32-shell-execute "open" $fpath))
         $file-list))
       ((string-equal system-type "darwin")
        (mapc
         (lambda ($fpath)
           (shell-command
            (concat "open " (shell-quote-argument $fpath))))
         $file-list))
       ((string-equal system-type "gnu/linux")
        (mapc
         (lambda ($fpath) (let ((process-connection-type nil))
                            (start-process "" nil "xdg-open" $fpath)))
         $file-list))))))

(define-key dired-mode-map (kbd "<C-return>") 'xah-open-in-external-app)

使用方法:按键 C-return 调用这个命令可以使用系统默认应用打开dired中光标所在文件或被标记的多个文件。

1.1.3 文件选择

选择多个文件的目的是批量操作,比如批量移动、批量删除等。选择文件的操作我们称为标记(mark),相关按键如下:

  • m: 标记光标所在的文件或目录,并将光标下移一行
  • DEL: 删除上一行标记,并将光标上移一行
  • u: 取消光标所在文件或目录的标记
  • U: 取消所有文件或目录的标记
  • t: 反选所有文件或目录的标记
  • #: 标记所有以 # 结尾的emacs临时文件
  • ~: 标记所有以 结尾的emacs自动备份文件
  • d: 给文件或目录加“待删除”标记(按'x'执行删除)

1.1.4 文件操作

按下快捷键时,dired优先操作有mark标记的文件,多个标记则为批量操作,没有标记则只对当前光标下的文件操作。常用的操作有:

  • +: 创建子目录
  • C: 拷贝文件或目录
  • R: 重命名/移动 文件或目录
  • D: 直接删除文件或目录
  • x: 删除带有“待删除”标记(d)的文件或目录
  • c: 压缩文件,默认可使用的后缀有 .zip, .tar.gz, .tar.bz2, .tar.xz, .tar.zst
  • Z: 使用gzip压缩或解压缩文件
  • g: 刷新dired buffer

1.2 文件侧边栏

Emacs中浏览文件侧边栏目录树的插件有好几个,比较常用的有 内置的speedbar、neotreetreemacs 等。这里我们介绍neotree。Neotree侧边栏效果如下图:

neotree.png

1.2.1 安装

将如下安装代码粘贴到 init-filemanage.el 文件中:

(use-package neotree
  :ensure t
  :init (setq neo-window-fixed-size nil
              neo-theme (if (display-graphic-p) 'icons 'arrow))
  :bind (("<f8>" . neotree-toggle)))

1.2.2 使用

  • <f8>: 打开neotree
  • p, n: 文件目录间上下移动
  • SPC/RET/TAB: 这三个快捷键都可以打开文件或展开目录
  • U: 跳转到上一级目录
  • g: 刷新
  • H: 显示或隐藏 隐藏文件(dotfiles)
  • O: 打开目录下的所有目录结构
  • A: 最大化/最小化neotree窗口
  • C-c C-n: 创建文件或目录(以"/"结尾)
  • C-c C-d: 删除文件或目录
  • C-c C-r: 重命名文件后目录
  • C-c C-c: 设置当前目录为展示的根目录
  • C-c C-p: 复制文件或目录

1.3 文件Tab栏

文件的Tab栏用于快速切换最近打开的文件,我们介绍 centaur-tabs 。centaur-tabs效果如下图:

tabs.png

1.3.1 安装

将如下安装代码粘贴到 init-filemanage.el 文件中:

(use-package centaur-tabs
  :ensure t
  :config
  (setq centaur-tabs-style "bar"
        centaur-tabs-height 22
        centaur-tabs-set-icons t
        centaur-tabs-plain-icons t
        centaur-tabs-gray-out-icons t
        centaur-tabs-set-close-button t
        centaur-tabs-set-modified-marker t
        centaur-tabs-show-navigation-buttons t
        centaur-tabs-set-bar 'left
        centaur-tabs-cycle-scope 'tabs
        x-underline-at-descent-line nil)
  (centaur-tabs-headline-match)
  ;; (setq centaur-tabs-gray-out-icons 'buffer)
  ;; (centaur-tabs-enable-buffer-reordering)
  ;; (setq centaur-tabs-adjust-buffer-order t)
  (centaur-tabs-mode t)
  (setq uniquify-separator "/")
  (setq uniquify-buffer-name-style 'forward)
  (defun centaur-tabs-buffer-groups ()
    "`centaur-tabs-buffer-groups' control buffers' group rules.
 Group centaur-tabs with mode if buffer is derived from `eshell-mode' `emacs-lisp-mode' `dired-mode' `org-mode' `magit-mode'.
 All buffer name start with * will group to \"Emacs\".
 Other buffer group by `centaur-tabs-get-group-name' with project name."
    (list
     (cond
      ((ignore-errors
         (and (string= "*xwidget" (substring (buffer-name) 0 8))
              (not (string= "*xwidget-log*" (buffer-name)))))
       "Xwidget")
      ((or (string-equal "*" (substring (buffer-name) 0 1))
           (memq major-mode '(magit-process-mode
                              magit-status-mode
                              magit-diff-mode
                              magit-log-mode
                              magit-file-mode
                              magit-blob-mode
                              magit-blame-mode
                              )))
       "Emacs")
      ((derived-mode-p 'prog-mode)
       "Editing")
      ((derived-mode-p 'dired-mode)
       "Dired")
      ((memq major-mode '(helpful-mode
                          help-mode))
       "Help")
      ((memq major-mode '(org-mode
                          org-agenda-clockreport-mode
                          org-src-mode
                          org-agenda-mode
                          org-beamer-mode
                          org-indent-mode
                          org-bullets-mode
                          org-cdlatex-mode
                          org-agenda-log-mode
                          diary-mode))
       "OrgMode")
      (t
       (centaur-tabs-get-group-name (current-buffer))))))
  :hook
  (dashboard-mode . centaur-tabs-local-mode)
  (term-mode . centaur-tabs-local-mode)
  (calendar-mode . centaur-tabs-local-mode)
  (org-agenda-mode . centaur-tabs-local-mode)
  (helpful-mode . centaur-tabs-local-mode)
  :bind
  ("C-c b" . centaur-tabs-backward)
  ("C-c n" . centaur-tabs-forward)
  ("C-c m" . centaur-tabs-forward-group)
  ("C-c v" . centaur-tabs-backward-group))

1.3.2 使用

  • C-c n: 切换到下一个tab
  • C-c b: 切换到上一个tab
  • C-c v: 切换到上一个分组
  • C-c m: 切换到下一个分组

1.4 文件(内容)搜索

1.4.1 搜索当前文件内容

在当前文件中快速搜索内容,使用 swiper ,将下面代码粘贴到 init-filemanage.el 中:

(use-package swiper
  ;; 快捷搜索
  :ensure nil
  :bind (("C-s" . swiper)))

按键 C-s 就可以搜索内容啦。

1.4.2 搜索最近访问的文件

定位最近访问的文件最快的方法就是切换buffer,使用 ivy-switch-buffercounsel-switch-buffer 。区别是,后者在buffer选项间移动时会实时的显示buffer的内容。读者可以尝试一下这两个命令,然后选择自己喜欢的方式绑定到快捷键。我用 ivy-switch-buffer 。将下面的代码粘贴到init-filemanage.el文件中:

(global-set-key (kbd "C-x b") 'ivy-switch-buffer)

这段按键绑定的代码应该不难理解吧,相信你也应该知道怎么修改来使用另一个命令。哈哈,其实emacs-lisp也没有那么难啦~

当然也可以在之前ivy的配置中使用bind参数来绑定快捷键,就像前面的centaur-tabs。我们后面也将使用这种方式。

1.4.3 搜索经常访问的文件

对于经常需要访问的文件,使用 bookmark ,下次访问时直接从bookmark列表打开。将下面的代码粘贴到 init-filemanage.el 文件中:

(use-package bookmark
  :ensure nil
  :bind (("C-x r m" . bookmark-set)
         ("C-x r d" . bookmark-delete)
         ("C-x r j" . bookmark-jump)))

按键 C-x r m 将当前文件加入bookmark,默认名称为文件名,也可以自己重命名。按键 C-x r d 选择一个bookmark删除。按键 C-x r j 选择一个bookmark打开。或者,使用 counsel-bookmark ,结合了创建和跳转bookmark两个功能,代码见下文。

1.4.4 搜索当前目录下文件内容

在当前目录内按照文件的内容查找,使用 counsel-rg 。由于该命令通过 rg 来实现搜索,所以在使用前需要先安装命令行工具 rg(ripgrep) 。MacOS下直接使用homebrew安装(其他系统请使用各自的包管理器安装):

$ brew install ripgrep

1.4.5 搜索当前目录下文件

在当前目录下按照文件名称查找文件,使用 counsel-fzf 。由于该命令通过 fzf 来实现搜索,所以在使用前需要先安装命令行工具 fzf

$ brew install fzf

1.4.6 搜索当前git仓库下文件

在当前git代码仓库中查找文件,使用 counsel-git 。当前文件若不在git仓库中则无法使用该命令。

以上三种搜索方式,基本可以满足在emacs中快速定位和查找文件的需求。将下面的代码粘贴到 init-filemanage.el 文件中,相应快捷键的使用不再赘述。

(use-package counsel
  :ensure t
  :bind (("M-x" . counsel-M-x)
         ("C-x C-f" . counsel-find-file)
         ("C-c c t" . counsel-load-theme)
         ("C-c c b" . counsel-bookmark)
         ("C-c c r" . counsel-rg)
         ("C-c c f" . counsel-fzf)
         ("C-c c g" . counsel-git)))

额外地, C-c c t 用于切换内置主题。

2 附加

2.1 Dired额外功能

与文件系统相关的操作:

  • H: 建立硬链接
  • S: 建立软链接
  • G: 改变文件Group
  • O: 改变文件Owner
  • M: 改变文件权限
  • P: 打印

Dired中除了使用标记来批量操作文件外,还可以使用正则表达式。正则操作的快捷键一般以 % 开头。

  • % m: 标记正则匹配的文件
  • % d: 给正则匹配的文件添加“待删除标记”(按键'x'执行删除)
  • % g: 根据正则表达式,搜索所有文件的内容,标记内容中有正则匹配的文件
  • % u: 所有标记的文件名称转化为大写
  • % l: 所有标记的文件名称转化为小写

对于已经标记的文件,我们可以不用打开文件,而对多个文件的内容进行搜索或替换操作。

  • A: 根据正则表达式搜索已标记的文件的内容,并列出所有匹配行
  • Q: 对标记的文件逐一进行正则替换,按键 "y" 替换,按键 "n" 跳过

dired还可以通过调用外部命令来操作文件。

  • !: 以同步的方式调用shell命令来操作文件,命令运行的工作目录就是dired的当前目录
  • &: 以异步的方式调用shell命令来操作文件,命令运行的工作目录就是dired的当前目录

2.2 Dired美化

嫌dired-mode颜色太单调?下面我们就给dired变个妆:

(use-package diredfl
  :ensure t
  :config (diredfl-global-mode t))

(use-package all-the-icons-dired
  :ensure t
  :config
  (add-hook 'dired-mode-hook 'all-the-icons-dired-mode))

all-the-icons-dired 会自动安装其依赖的 all-the-icons package,安装成功后通过按键 M-x all-the-icons-install-fonts 安装必要的字体(此操作需要科学上网)。 没办法科学上网的同学可以直接将 字体 下载到本地后添加到系统的字体目录中,如何添加自行搜索。

美化后的效果:

beautiful-dired.png

3 结语

断断续续,花了一周的时间梳理文件管理的工作流。希望大家能够积极留言,说说有哪些对于新手难以理解的地方,有哪些其他的文件管理方面的需求。有不合理的地方也敬请指出,我将根据大家的反馈不断完善文章的内容。

At last, happy hacking emacs!

发布于 2020-07-05

Emacs Hack - 分割文件相似内容

实际问题

将一个文件中的相似块分割成若干个文件。比如下面这个例子:

第一回: 灵根育孕源流出 心性修持大道生
  这是第一段内容.....
  这是第二段内容.....
  这是第三段内容.....
  更多...

第二回: 悟彻菩提真妙理 断魔归本合元神
  这是第一段内容.....
  这是第二段内容.....
  这是第三段内容.....
  更多...

第三回: 四海千山皆拱伏 九幽十类尽除名
  这是第一段内容.....
  这是第二段内容.....
  这是第三段内容.....
  更多...

假设有一个长篇小说txt文件,每一回有相似的结构。要解决的问题是:如何将每一回分割成单独的文件?文件名是每一回的标题,文件的内容就是每一回的内容。

解决思路

解决这种实际的问题,我的思路一般是:用人的思维找到直观的解决办法。然后,将人的思维转换成对应的代码逻辑,比如:运用什么控制语句?使用什么数据结构?涉及什么特殊算法?等等。

首先我们从人的思维出发考虑如何解决这个问题。最直接的方法是:根据开头“第一回:”这几个字确定这一回的标题,然后确定接下来直到“第二回:”之间的部分为第一回的内容。然后将第一回的内容写到以标题命名的文件中。接下里,以此类推,直到最后没有内容。

接下来,思考如何将这个过程转化成代码的逻辑。

显然,这里是循环判断的逻辑,循环的范围是从第一行到最后一行。分析过程如下:

  1. 搜索第一行,是标题行,记录标题并移到下一行。
  2. 搜索这一行,不是标题行,记录行首的位置start。判断下一行是否是标题行,不是则移到下一行。
  3. 重复过程2,直到当前行的下一行是标题行时,记录本行行首的位置end。
  4. 将start和end之间的内容写到以标题命名的文件中。移到下一行。
  5. 重复上面的过程1,2,3,4。直到光标到达最后。

代码实现

根据上面的分析过程,有如下的代码:

(defun my-split-file (file)
  (interactive "fchoose a file: ")
  (let ((filedir "~/test-dir/")
        filename start end content)
    (with-current-buffer (get-buffer-create "*split-file*")
      (insert-file-contents file)
      (goto-char (point-min))
      (while (< (point) (point-max))
        (if (search-forward-regexp "^第.+回:" (line-end-position) t)
            (progn 
              (setq filename (string-trim (thing-at-point 'line))) 
              (next-line)
              (beginning-of-line)
              (setq start (point))) ;; 获取内容的起始位置start
          (save-excursion ;; 保存光标的位置:只判断下一行的情况,不改变实际要操作的光标。
            (next-line)
            (beginning-of-line)
            (when (or (search-forward-regexp "^第.+回:" (line-end-position) t)
                      (= (point) (point-max)))
              (previous-line)
              (end-of-line)
              (setq end (point)) ;; 获取内容的结尾位置end
              (setq content (buffer-substring-no-properties start end)) ;; 获取start和end之间的内容。
              (with-temp-file (concat filedir filename ".txt")
                (insert content)) ;; 写文件
              ))
          (next-line)
          (beginning-of-line))))))
发布于 2020-06-28

Emacs Workflow - 准备工作

众所周知,emacs以学习曲线陡峭著称,所以在开始介绍workflow前,得做些准备工作。这篇文章主要介绍emacs的安装、基础概念、基础配置和使用emacs的基础知识。

安装

Emacs的安装非常简单,各个平台的安装方法详见官网。我使用的是MacOS,通过homebrew安装 emacs-plus 27,支持xwidget webkit。安装代码如下:

$ brew tap d12frosted/emacs-plus
$ brew install emacs-plus@27 --with-xwidgets

MacOS Homebrew 的安装方法见 brew.sh

快捷键

Emacs和其他编辑器的相比一个重要的优势就是全键盘操作。所以在使用emacs之前,先通过按键 C-h t 查看内置的快捷键教程是一个较好的开端。下面对基本快捷键做简要介绍:

Emacs的快捷键都是组合键,由前缀键加上字母或数字组成。常见的前缀键有:

  • M 对应 Alt
  • C 对应 Control
  • S 对应 Shift

形如 C-x C-f 的快捷键表示按住 Ctrl 键时按下字母 X , 再按住 Ctrl 键同时按下字母 F

下面是一些常用快捷键:

快捷键 功能 快捷键 功能
C-x C-c 关闭Emacs 编辑  
C-g 取消当前按键输入 C-@C-SPC 设置mark
    M-w 复制
移动   C-w 剪切
C-p 光标上移 C-y 粘贴
C-n 光标下移 C-x h 全选
C-b 光标左移 C-x C-q 切换只读/编辑模式
C-f 光标右移 C-/ 撤销上一步操作
C-a 光标移到行首    
C-e 光标移到行尾 缓冲区  
M-b 光标前移一个单词 C-x b 切换buffer
M-f 光标后移一个单词 C-x k 关闭buffer
C-v 向下翻页 C-x C-b 查看所有打开的缓冲区
M-v 向上翻页    
C-l 光标移到屏幕上/中/下部 窗口  
C-x [ 光标跳到文首 C-x 2 在下面分割一个窗口
C-x ] 光标跳到文末 C-x 3 在右边分割一个窗口
    C-x 0 关闭当前窗口
文件   C-x 1 关闭其它窗口
C-x C-f 打开(创建)文件 C-x o 依次切换窗口
C-x C-s 保存文件    

最小配置

Emacs使用Emacs-Lisp作为配置语言,通过添加配置可以使emacs符合个人的使用习惯和实现各种功能。通常,emacs的配置文件放置在用户根目录的 .emacs.d 文件夹中。其中的 init.el 作为配置的入口文件。

下面提供了一段最小配置(参考 better-defaults ),直接将它复制粘贴到 init.el 文件中后,重启emacs。

  ;;; better defaults
(unless (or (fboundp 'helm-mode) (fboundp 'ivy-mode))
  (ido-mode t)
  (setq ido-enable-flex-matching t)) ;; 使用ido补全

(unless (eq window-system 'ns)
  (menu-bar-mode -1)) ;; 禁用菜单栏
(when (fboundp 'tool-bar-mode)
  (tool-bar-mode -1)) ;; 禁用工具栏
(when (fboundp 'scroll-bar-mode)
  (scroll-bar-mode -1)) ;; 禁用垂直滚动条
(when (fboundp 'horizontal-scroll-bar-mode)
  (horizontal-scroll-bar-mode -1)) ;; 禁用水平滚动条

(require 'uniquify)
(setq uniquify-buffer-name-style 'forward)

;; https://www.emacswiki.org/emacs/SavePlace
(save-place-mode 1) ;; 保存光标文位置

(global-set-key (kbd "C-s") 'isearch-forward)
(global-set-key (kbd "C-r") 'isearch-backward)
(global-set-key (kbd "C-c C-/") 'comment-or-uncomment-region) ;; 代码块注释和反注释

(show-paren-mode 1) ;; 显示括号匹配
(setq-default indent-tabs-mode nil)
(setq save-interprogram-paste-before-kill t
      apropos-do-all t
      mouse-yank-at-point t
      require-final-newline t
      load-prefer-newer t
      ediff-window-setup-function 'ediff-setup-windows-plain)

(require 'dired-x)
(delete-selection-mode 1) ;; 选择后插入,删除原字符。
(recentf-mode 1) ;; 保存最近访问
(global-auto-revert-mode 1) ;; 自动加载更新内容
(fset 'yes-or-no-p 'y-or-n-p) ;; 使用 'y/n' 代替 'yes/no'
(setq custom-file (concat user-emacs-directory "custom.el"))
(setq inhibit-startup-message t) ;; 禁止启动信息
(setq ring-bell-function 'ignore) ;; 禁止发出声音警告
(setq make-backup-files nil) ;; 不允许备份
(setq auto-save-default nil  ;; 不允许默认自动保存
      auto-save-silent t))   ;; 自动保存时不显示消息
(setq scroll-step 1 scroll-margin 3 scroll-conservatively 10000) ;; 连续滚动
(setq confirm-kill-emacs
      (lambda (prompt) (y-or-n-p-with-timeout "Whether to quit Emacs:" 10 "y"))) ;; 防误操作退出
(setq dired-recursive-deletes 'always
      dired-recursive-copies 'always) ;; 全部递归拷贝、删除文件夹中的文件

包管理

Emacs的package(也就是我们通常说的"包"或"插件")可以为emacs拓展丰富多样的功能。为了能够使用这些package,需要配置获取package的源。在init.el的最后加上以下代码:

(setq package-enable-at-startup nil)
(setq package-archives '(("gnu" . "http://mirrors.cloud.tencent.com/elpa/gnu/")
                         ("melpa" . "http://mirrors.cloud.tencent.com/elpa/melpa/")))

配置好源后,按键 M-x list-packages 可以查看所有已发布的package。按键 M-x package-install 后输入package名字可以直接安装,同理使用 package-delete 删除。使用package,需要先在配置文件中写入 (require '<package-name>) 这个过程相当于导入(import)。再加上必要的自定义配置便可使用该package所有的功能。

以上的包管理方案由emacs内置的 package.el 提供。但内置的不一定是最好的。因此,有一些package专门提供了更加灵活、自动化的包管理方案。常用的有 use-package quelpa straight el-get 等,我使用的是 use-package 结合 git submodule 。下面的代码用于初始化 use-package ,加入init.el结尾。

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

写完配置代码后,在最后一个括号后面按键 C-x C-e (eval-last-sexp)即可执行配置,安装package。也可以重启emacs,再次打开时emacs会自动加载所有配置。

配置管理

值得注意的是,我们将上面的配置代码统统写入了init.el文件中。可以预见,当安装许多package时,配置代码将会增多,init.el的内容会变得复杂无比,难以阅读和维护。我们需要一种合理的组织配置文件的方式。

解决方法是将每一种workflow的配置代码写在单独的文件中,然后在init.el中引入该文件。操作如下:

  1. 在.emacs.d文件夹下创建elisp文件夹。
  2. 在init.el中添加代码 (add-to-list 'load-path (concat user-emacs-directory "elisp"))
  3. 在elisp文件夹下创建 init-better.el ,将“最小配置”的代码粘贴进去。
  4. init-better.el 最后加上代码 (provide 'init-better)
  5. init.el 最后加上代码 (require 'init-better)

根据字面意思也不难理解:步骤2的代码将elisp文件夹下的所有文件加入配置加载路径;步骤4的provide提供文件名,使其可以被引入;步骤5的require引入了该文件。这样我们就将最小配置的代码引入到init.el中了。以后的各种workflow我们也将使用这种方式来组织配置文件。

实用package

介绍一些对于新手实用的package,直接将下面的配置粘贴到elisp文件夹下的 init-utils.el 文件中。

(use-package super-save
  ;; 自动保存,用于替换默认的自动保存
  :ensure t
  :config
  (super-save-mode +1)
  (setq super-save-auto-save-when-idle t))

(use-package which-key
  ;; emacs按键提示
  :ensure t
  :config
  (which-key-mode))

(use-package ivy
  ;; emacs补全框架
  :ensure t
  :init
  (setq ivy-use-virtual-buffers t
        enable-recursive-minibuffers t)
  :config
  (ivy-mode 1))

同理,在配置init-utils.el文件结尾加上 (provide 'init-utils) ,然后在init.el中引入 (require 'init-utils)

emacs主题

选择一个简洁、美观的主题不仅可以缓解眼睛疲劳,还可以提高使用emacs的效率。emacs的主题分为亮色和暗色两种,我的使用习惯是白天使用亮色主题,晚上使用暗色主题。也可以选择喜欢的第三方主题安装。我最喜欢的亮色主题是leuven(内置),暗色主题是dracula(第三方)。

全部配置代码

我建议你按照教程的步骤,一步步拷贝、粘贴、执行代码。这个过程中,你会了解到如何从零配置一个功能强大的emacs编辑器,如何像搭积木一样通过添加配置文件使emacs充满无限的可能性。所有的配置代码我也会放在 emacs-workflow-config 这个代码仓库,读者可以直接把它克隆到 .emacs.d 文件夹下使用。

结语

如何配置一个舒适易用的emacs环境是一个大话题,有很多非常nice的package,但考虑到这大多数是与“提高编程的体验”相关,并不是Emacs Workflow的重点。所以,这一篇中我只介绍一个最小配置和部分实用(必要)的package,更多的优化配置不多讲解。好啦,以上就是使用emacs前的准备工作,接下来就可以愉快的学习各种工作流啦!

发布于 2020-06-25

Emacs Hack - 批量替换字符串

实际问题

不使用interactive function(M-%)如何批量替换文件中的字符串?应用场景为文字校验。

解决思路

字符串替换分三种情况:

  1. 在当前buffer替换
  2. 在指定文件中替换
  3. 为指定目录下所有文件替换。

用于替换的pair有两种来源:

  1. repl-string-listrepl-regexp-list 变量
  2. repl-file 对应的文件。

字符串查找的方式有两种,分别对应两个函数:

  1. 纯字符串 (replall-string)
  2. 正则表达式 (replall-regexp)

优先使用列表变量的值,如果该变量值为nil则使用repl-file对应的文件中的值。其中文件中的pair只能用于 replall-string 函数。两个变量列表的值分别对应两个函数。repl-file的格式为每行一个pair,分别为被替换的字符串和用于替换的字符串,用空格隔开。

代码实现

主函数 replall-stringreplall-regexp 可以选择替换类型。对于具体的情况也可以直接使用具体的替换函数。代码如下:

(setq repl-string-list
      '(("old" "new")
        ("test" "测试")
        ("错误" "right")
        ("隔开你" "戈楷旎")))

(setq repl-regexp-list
      '(("\\." "。")))

(setq repl-file "~/replace.txt")

(defun replall--read-pair-from-file ()
  (let ((repl-list '()))
    (with-temp-buffer
      (insert-file-contents repl-file)
      (goto-char (point-min))
      (while (< (point) (point-max))
        (setq repl-pair (split-string (thing-at-point 'line) "[ \f\t\n\r\v]+" t "[ \f\t\n\r\v]+"))
        (if (null repl-pair)
            (next-line)
          (next-line)
          (setq repl-list (append repl-list (list repl-pair))))))
    repl-list))

(defun replall--get-repl-string-list ()
  (if (bound-and-true-p repl-string-list)
      repl-string-list
    (replall--read-pair-from-file)))

(defun replall--get-repl-regexp-list ()
  (if (bound-and-true-p repl-regexp-list)
      repl-regexp-list
    (message "please set variable 'repl-regexp-list'!")))

(defun replall--string (file lst)
  (with-temp-buffer
    (insert-file-contents file)
    (goto-char (point-min))
    (dolist (pair lst)
      (while (search-forward (car pair) nil t)
        (replace-match (cadr pair)))
      (goto-char (point-min)))
    (write-file file)))

(defun replall--regexp (file lst)
  (with-temp-buffer
    (insert-file-contents file)
    (goto-char (point-min))
    (dolist (pair lst)
      (while (re-search-forward (car pair) nil t)
        (replace-match (cadr pair)))
      (goto-char (point-min)))
    (write-file file)))

(defun replall-string-in-curr-buffer ()
  (interactive)
  (let ((curr-file (buffer-file-name (current-buffer)))
        (repl-list (replall--get-repl-string-list)))
    (replall--string curr-file repl-list)))

(defun replall-regexp-in-curr-buffer ()
  (interactive)
  (let ((curr-file (buffer-file-name (current-buffer)))
        (repl-list (replall--get-repl-regexp-list)))
    (replall--regexp curr-file repl-list)))

(defun replall-string-in-file (file repl)
  (interactive "fchoose a file to be processed: ")
  (let ((repl-list (replall--get-repl-string-list)))
    (replall--string file repl-list)))

(defun replall-regexp-in-file (file repl)
  (interactive "fchoose a file to be processed: ")
  (let ((repl-list (replall--get-repl-regexp-list)))
    (replall--regexp file repl-list)))

(defun replall--get-real-files-in-dir (dir)
  (let ((real-files)
        (files (directory-files dir)))
    (dolist (file files)
      (when (not (or (string= "." (substring file 0 1))
                     (string= "#" (substring file 0 1))
                     (string= "~" (substring file -1))))
        (push file real-files)))
    real-files))

(defun replall-string-in-directory (dir)
  (interactive "Dchoose a directory to be processed: ")
  (let* ((repl-list (replall--get-repl-string-list))
         (real-files (replall--get-real-files-in-dir dir)))
    (dolist (file real-files)
      (replall--string (concat dir file) repl-list))))

(defun replall-regexp-in-directory (dir)
  (interactive "Dchoose a directory to be processed: ")
  (let* ((repl-list (replall--get-repl-regexp-list))
         (real-files (replall--get-real-files-in-dir dir)))
    (dolist (file real-files)
      (replall--regexp (concat dir file) repl-list))))

(defun replall-string (type)
  (interactive "sreplace string: 1.in current buffer  2.in a file  3.in a directory (input 1~3): ")
  (cond
   ((string= type "1")
    (replall-string-in-curr-buffer))
   ((string= type "2")
    (call-interactively #'replall-string-in-file))
   ((string= type "3")
    (call-interactively #'replall-string-in-directory))
   (t (message "please input 1~3!"))))

(defun replall-regexp (type)
  (interactive "sreplace regexp: 1.in current buffer  2.in a file  3.in a directory (input 1~3): ")
  (cond
   ((string= type "1")
    (replall-regexp-in-curr-buffer))
   ((string= type "2")
    (call-interactively #'replall-regexp-in-file))
   ((string= type "3")
    (call-interactively #'replall-regexp-in-directory))
   (t (message "please input 1~3!"))))
发布于 2020-06-16

Emacs Workflow - 开门见山

Emacs Workflow 顾名思义 “emacs工作流”。这一系列的文章主要介绍emacs中的常用的工作流,比如:文件管理、日程管理、笔记管理、听音乐、网页浏览、收发邮件、版本控制、字典查阅、快捷搜索、代数计算、telegram… 每一种工作流都可以替代一种额外的app或系统应用。最终,我们逐渐向 "live in emacs" 的目标靠近。

在介绍具体的emacs workflow之前,我觉得有必要先搞清楚下面几个问题。

什么是Emacs?

官网的介绍是 "An extensible, customizable, free/libre text editor — and more"。这里有5个关键词:可拓展、可定制、自由、编辑器、更多。

emacs本质上是一个 编辑器 ,从这个意义上来说,它和办公中经常使用的 MS word,程序员使用的vim, notepad别无二致。 可拓展 意味着我们可以通过程序来实现新的功能、特性。 可定制 意味着我们可以通过配置使emacs符合个人使用习惯。 自由 说的是emacs是一个自由软件(非商业)。 更多 包含了不计其数的可能性。

上面的解释可以总结为一句话: "If related data, nothing is impossible in Emacs."。你可能觉得这有点夸张,但我可以解释给你听:

这是个物质的世界,也是个数据的世界。物质的联系产生了数据,数据的改变塑造了物质。数据的种类有很多:简单的阿拉伯数字、数学公式、化学方程式、计算机程序、历史资料、软件密码、基因序列、社会信息…这些都是数据。因此,我们可以简单的得出一个结论,改变数据就可以改变一切。

编辑器的作用是编辑数据,处理文本。普通的编辑器只能对数据进行简单的处理,比如插入、删除、改变样式、公式计算等等。而Emacs的高度可拓展性为数据处理提供了无限的可能。使用elisp,调用外部程序,emacs可以为数据之间创造复杂的逻辑,实现各种功能,满足各种需求,从而无所不能。

我们可以随便举一个看似不可能在emacs中完成的例子,看看emacs如何把不可能变成可能。比如:用emacs来煮咖啡。值得注意的是,使用emacs的前提是 "data related",即能够与emacs交换数据信息。所以,假设我们有一个智能咖啡机,可以通过外部指令控制。现在我们可以编写一个 make-coffee 函数执行煮咖啡的指令,然后 M-x make-coffee ,大功告成!这是一个看似很简单的例子,但却揭示了emacs的本质: Emacs是一个使用程序来处理数据的万能前端 。这一点和操作系统很像。

以上就是我对Emacs的理解。

什么样的人适合使用Emacs?

很多人认为Emacs是程序员、hacker的专利,我不认同。我觉得Emacs可以是大众的编辑器,可以服务于我们每一个普通人。无论你的职业为何,只要生活和工作离不开计算机,Emacs都可以成为你提升效率的大杀器。从这个以上意义上来说, Emacs适合每一个经常和计算机打交道的人

为什么我推荐你使用Emacs?

我曾经向很多熟悉的,不熟悉的人安利过emacs,但成功的情况很少。原因在于,我总是会像上面"什么是Emacs?"中一样,把它介绍的很牛逼,因此让人望而生畏。事实上,emacs确实很牛逼,但牛逼的东西也是由简单的事物构成的,就像一个复杂的计算机系统是由简单的 0、1 组成。

不同职业、不同身份的人使用计算机的需求和程度不一样,学习的计算机知识的难度也不同。用计算机办公、处理邮件的人只需要学习office办公软件,学会收发邮件;用计算机制作和处理图片、音视频的人只需要学习诸如PS、PR、AE等专业软件;普通程序员需要了解计算机运行的原理,学习如何通过特定的程序语言与计算机交互;高级黑客需要学习计算机底层架构和原理,了解程序语言背后的逻辑 ……

很少人因为计算机是个复杂的东西就放弃使用它,因为你不需要了解它的全部,只需学习你需要使用的部分;因为只要你用它,就会给工作和生活带来极大的便利。这句话用在Emacs上也再合适不过了!

这就是我推荐你使用Emacs的全部理由。

这是怎样的Emacs教程?

Emacs Workflow 系列的文章会更关注新手,因此概念解释会比较详细。一个完整的工作流包含与之相关的方方面面,我将在我知识所及的情况下尽可能的介绍全面。工作流的不同方面可能会使用不同的emacs package或我自己编写的elisp代码,对于package或代码的内容,新手无需理解,拷贝粘贴后掌握如何使用即可。工作流的介绍分为“基础”和“附加”两部分。附加部分介绍不常用的或与程序相关的功能,这部分新手可以直接跳过。实现某一特定功能的package可能不只一个,我的解决方案可能不是最优解,欢迎留言补充。

最后,希望我的工作对你有所帮助,希望emacs伴你走过每一个春夏秋冬~

发布于 2020-06-14

基于Emacs-Lisp的HTML模版库

English Document

1 介绍

pp-html 是使用elisp开发的html模版语言。它借鉴了部分 Liquid template language 的设计思路,包括对象,标签和过滤器三部分。通过书写elisp的S表达式,读者可以快速便捷地构建简单html片段或复杂的html页面。其中 :include:extend (包含和继承) 标签使得模块化构建html成为可能,提高了代码重用率。

2 安装

克隆此代码仓库:

$ git clone https://github.com/Kinneyzhang/pp-html.git <path-to-pp-html>

安装相关依赖: dash, sweb-mode

然后在emacs配置中添加如下两行:

(add-to-list 'load-path "<path-to-pp-html>")
(require 'pp-html)

3 使用

3.1 基础

pp-html使用elisp的S表达式来构建html代码,使用 pp-html 函数对S表达式求值。下面有一些简单的例子,便于理解基本的使用方法。

3.1.1 单个S表达式

单个S表达式的构成为: (html标签 多个属性键值对 内容)

其中html标签是必须的,其余可缺省。属性键值对的书写语法为Plist形式。特别地是,对CSS选择器设置了语法糖,用 "." 表示 "class","@" 表示 "id",其值为符号后面的内容。对于无值属性(例如 async)有两种写法:1. (:async nil) 2. 直接写:async,要求不能是最后一个属性。

(pp-html '(a "content"))
(pp-html '(a @id .class))
(pp-html '(a :id "id" :class "class"))
(pp-html '(a .test :href "url" :target "_blank" "content"))
(pp-html '(link :async :rel "stylesheet" :href "url" :async nil))
<a>content</a>
<a id="id" class="class"></a>
<a id="id" class="class"></a>
<a class="test" href="url" target="_blank">content</a>
<link async rel="stylesheet" href "url" async/>

3.1.2 并列S表达式

并列的多个S表达式直接用括号包含。

(pp-html
 '((div .div1 "div-content")
   (p "paragraph")
   (a :href "url" "a-content")
   (img :src "path")
   (ul .org-ul
       (li "1")
       (li "2")
       (li "3"))))
<div class="div1">div-content</div>
<p>paragraph</p>
<a href="url">a-content</a>
<img src="path"/>
<ul class="org-ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

3.1.3 嵌套S表达式

在同一html标签的S表达式内代表该元素的子元素,否则为兄弟元素。

(pp-html
 '(div .container
       (div .row
            (div .col-8
                 (p "paragraph 1"))
            (div .col-4
                 (p "paragraph 2")))))
<div class="container">
  <div class="row">
    <div class="col-8">
      <p>paragraph 1</p>
    </div>
    <div class="col-4">
      <p>paragraph 2</p>
    </div>
  </div>
</div>

3.2 对象

对象告诉pp-html在页面何处显示对象的值。它包括三类:变量求值、对象属性求值和函数求值。可以使用 pp-html-eval 函数获取对象的值。

3.2.1 变量求值

变量求值的基本语法是在变量前加上"$"符号。

(let ((var1 "happy hacking emacs"))
  (pp-html-eval '$var1))
happy hacking emacs

变量可应用于S表达式的任何部分。

(let ((url "https://geekinney.com/")
      (name "戈楷旎"))
  (pp-html '(a :href $url $name)))
<a href="https://geekinney.com/">戈楷旎</a>

3.2.2 对象属性求值

特别地,对于Plist对象使用"."来获取属性值。

(let ((site '(:name "戈楷旎" :domain "geekinney.com" :author "Geekinney")))
  (pp-html '(div .site-info
                 (p $site.name)
                 (p $site.domain)
                 (p $site.author))))
<div class="site-info">
  <p>戈楷旎</p>
  <p>geekinney.com</p>
  <p>Geekinney</p>
</div>

3.2.3 函数求值

函数求值的S表达式语法为 ($ <function> <args…>), 函数的参数也可写成变量形式。

(let ((var1 "happy")
      (var2 " hacking"))
  (pp-html-eval '($ concat $var1 $var2 " emacs")))
happy hacking emacs

函数可嵌套调用,或直接写,两种写法等价。

(let ((var1 "now")
      (var2 " is ")
      (now '(current-time)))
  (pp-html-eval '($ concat ($ upcase $var1) $var2 ($ format-time-string "%Y-%m-%d" $now)))
  (pp-html-eval '($ concat (upcase $var1) $var2 (format-time-string "%Y-%m-%d" $now))))
NOW is 2020-05-10
NOW is 2020-05-10

同理,函数也可用于S表达式的任何部分,这样pp-html就可以任意使用elisp丰富强大的函数库了。

3.3 标签

标签为模版创造了逻辑和流程控制,它用冒号表示并且放在S表达式的第一个位置: (:tag …)。标签分为5类:

  • 变量定义
  • 流程控制
  • 迭代
  • 代码块

3.3.1 变量定义

assign

定义变量,相当于elisp的let或setq。

(pp-html
 '((:assign str1 "happy"
            str2 "hacking"
            str3 "emacs")
   (p ($ concat $str1 " " $str2 " " $str3))))
<p>happy hacking emacs</p>

3.3.2 流程控制

if

如果条件为真执行第一个代码块,否则执行第二个

(pp-html
 '((:assign bool nil)
   (:if $bool (p "true")
        (p "false"))))
<p>false</p>

unless

和if相反,如果条件为假,执行第一个代码块,否则执行第二个。

(pp-html
 '((:assign bool nil)
   (:unless $bool (p "true")
        (p "false"))))
<p>true</p>

cond

执行每一个分支,直到条件满足,执行满足条件的代码块。

(pp-html
 '((:assign case "case3")
   (:cond
    ($ string= $case "case1") (p "case1 branch")
    ($ string= $case "case2") (p "case2 branch")
    ($ string= $case "case3") (p "case3 branch")
    t (p "default branch"))))
<p>case3 branch</p>

3.3.3 迭代

for

for循环

(pp-html
 '((:assign editors ("vim" "emacs" "vscode"))
   (ul
    (:for editor in $editors
          (li :id $editor $editor)))))
<ul>
  <li id="vim">vim</li>
  <li id="emacs">emacs</li>
  <li id="vscode">vscode</li>
</ul>

3.3.4 代码块

include

在一个代码块中包含另一个代码块。

(setq block1
      '(p "block1 content"
          (a :href "url" "content")))

(setq block2
      '(div .block2
            (p "block2 content")
            (:include $block1)))

(pp-html block2)
<div class="block2">
  <p>block2 content</p>
  <p>
    block1 content
    <a href="url">content</a>
  </p>
</div>

extendblock

代码块继承。如果新代码块重写了 :block 标签之间的内容,覆盖原代码块对应的部分,其余保持不变。

(setq base-block '(p .base
                     (:block block-name (span "base content")))
      extend-block1 '(:extend $base-block
                              (:block block-name))
      extend-block2 '(:extend $base-block
                              (:block block-name
                                      (span "extended content"))))
(pp-html
 '((div "extend the default"
        (:include $extend-block1))
   (div "extend with new"
        (:include $extend-block2))))
<div>
  extend the default
  <p class="base">
    <span>base content</span>
  </p>
</div>
<div>
  extend with new
  <p class="base">
    <span>extended content</span>
  </p>
</div>

3.4 过滤器

过滤器的语法形式为 (/ <value> <:filter args> …)。过滤器作用于<value>,可以有参数,也可以没有。

3.4.1 自定义过滤器

pp-html支持自定义过滤器,使用 pp-html-define-filter 函数,它有两个参数:过滤器名称和过滤函数。例:

(pp-html-define-filter :add 'pp-html-filter-add)
(defun pp-html-filter-add (value arg)
  "Add a value to a number"
  (let ((arg (if (stringp arg)
                 (string-to-number arg)
               arg)))
    (+ value arg)))

3.4.2 内置过滤器

abs: 取绝对值

(pp-html-eval '(/ -5 :abs)) ;; => 5

add: 加上一个数

(pp-html-eval '(/ 4 :add 5)) ;; => 9

append: 结合两个列表

(let ((list1 '(1 2 3))
      (list2 '(5 6 7)))
  (pp-html-eval '(/ $list1 :append $list2))) ;; => (1 2 3 5 6 7)

capitalize: 第一个单词首字母大写

(pp-html-eval '(/ "happy hacking emacs!" :capitalize)) ;; => Happy hacking emacs!

compact: 删除列表中所有的nil

(let ((lst '(nil 1 2 nil 3 4 nil)))
  (pp-html-eval '(/ $lst :compact))) ;; => (1 2 3 4)

concat: 字符串连接

(let ((str1 "happy hacking ")
      (str2 "emacs"))
  (pp-html-eval '(/ $str1 :concat $str2))) ;; => happy hacking emacs

default: 不是nil或空字符串,设为默认值

(let ((str1 "")
      (str2 "new value")
      (lst1 '(1 2 3))
      (lst2 nil))
  (pp-html-eval '(/ $str1 :default "default value")) ;; => default value
  (pp-html-eval '(/ $str2 :default "default value")) ;; => new value
  (pp-html-eval '(/ $lst1 :default (4 5 6))) ;; => (1 2 3)
  (pp-html-eval '(/ $lst2 :default (4 5 6))) ;; => (4 5 6)
  )

escape: html特殊字符转义

(pp-html-eval '(/ "Have you read 'James & the Giant Peach'?" :escape)) ;; => Have you read &apos;James &amp; the Giant Peach&apos;?

join: 使用分隔符连接列表中字符串

(let ((lst '("happy" "hacking" "emacs")))
  (pp-html-eval '(/ $lst :join "-"))) ;; => happy-hacking-emacs

More useful filters are on the way!

3.5 综合

综合以上语法的例子:

(setq assign-vars
      '(:assign name "geekinney blog"
                description "Emacs is a lifestyle :-) And happy hacking emacs!"
                menus ((:path "/" :name "Index")
                       (:path "/archive" :name "Archive")
                       (:path "/category" :name "Category")
                       (:path "/about" :name "About"))
                comment-p t
                comment-type "disqus"
                valine-block (p "this is valine block")
                disqus-block (p "this is disqus block")))
(setq header-block
      '(header @topheader
               (a @logo :href "/" $name)
               (p .description $description)))

(setq menu-block
      '(nav @topmenu
            (:for menu in $menus
                  (a :href $menu.path $menu.name))))

(setq article-block
      '(article
        (p ($ concat "Function: the site name is " ($ upcase $name)))
        (p (/ "Filter: the site name is " :concat (/ $name :capitalize)))
        (p (/ ("happy" "hacking" "emacs") :join " " :capitalize :concat "!"))))

(setq comment-block
      '(div @comment
            (:if comment-p
                 (:cond
                  ($ string= $comment-type "valine") (:include $valine-block)
                  ($ string= $comment-type "disqus") (:include $disqus-block)
                  t nil)
                 (p "The comment is closed!"))))

(setq side-block
      '(aside @sidebar
              (:block side-block
                      (p "this is base sidebar"))))

(setq footer-block
      '(:block footer-block
               (footer
                (p "this is base footer."))))

(setq base-block
      '((:include $assign-vars)
        (body
         (div .container
              (div .row
                   (div .col-12
                        (:include $header-block)))
              (div .row
                   (div .col-12
                        (:include $menu-block)))
              (div .row
                   (div .col-12 .col-sm-12 .col-md-8 .col-lg-8
                        (:include $article-block)
                        (:include $comment-block))
                   (div .col-md-4 .col-lg-4
                        (:include $side-block)))
              (div .row
                   (div .col-12
                        (:include $footer-block)))))))

(pp-html
 '(:extend $base-block
           (:block side-block
                   (p "this is extended sidebar"))
           (:block footer-block)))
<body>
  <div class="container">
    <div class="row">
      <div class="col-12">
        <header id="topheader">
          <a id="logo" href="/">geekinney blog</a>
          <p class="description">Emacs is a lifestyle :-) And happy hacking emacs!</p>
        </header>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <nav id="topmenu">
          <a href="/">Index</a>
          <a href="/archive">Archive</a>
          <a href="/category">Category</a>
          <a href="/about">About</a>
        </nav>
      </div>
    </div>
    <div class="row">
      <div class="col-12 col-sm-12 col-md-8 col-lg-8">
        <article>
          <p>Function: the site name is GEEKINNEY BLOG</p>
          <p>Filter: the site name is Geekinney blog</p>
          <p>Happy hacking emacs!</p>
        </article>
        <div id="comment">
          <p>this is disqus block</p>
        </div>
      </div>
      <div class="col-md-4 col-lg-4">
        <aside id="sidebar">
          <p>this is extended sidebar</p>
        </aside>
      </div>
    </div>
    <div class="row">
      <div class="col-12">
        <footer>
          <p>this is base footer.</p>
        </footer>
      </div>
    </div>
  </div>
</body>

4 说明

4.1 预览调试

pp-html-test 函数可以在view buffer中预览生成的格式化html。 pp-html-parse 函数可以查看解析完所有逻辑标签后的S表达式。这两个函数便于调试代码。

4.2 XML支持

pp-html还额外支持生成xml。与html不同,xml没有单元素(img,link…),所以更简单。使用方法为设置 pp-html 函数的第二个参数为t。

4.3 结合OrgMode

在Org文件中,使用带参数的emacs-lisp代码块可以在Org或HTML中生成elisp代码对应的HTML。例如:

1.当导出Org文件时,生成一个有红色背景div的html页面。

#+BEGIN_SRC emacs-lisp :results value html :exports results
(pp-html '(div :style "background-color:red;" "content"))
#+END_SRC

#+RESULTS:
#+begin_export html
<div style="background-color:red;">content</div>
#+end_export

2.当导出Org文件时,生成包含 <div style="background-color:red;">content</div> 代码的html页面。

#+BEGIN_SRC emacs-lisp :wrap src html :exports results
(pp-html '(div :style "background-color:red;" "content"))
#+END_SRC

#+RESULTS:
#+begin_src html
<div style="background-color:red;">content</div>
#+end_src

关于OrgMode导出代码块的参数设置参考 Working-with-Source-Code

4.4 构建博客

我的 个人博客 就是基于 pp-html 构建的,我将构建博客的代码组织成了emacs包: geekblog ,目前处理代码优化整理阶段,敬请关注 我的Github 或博客。

5 计划

  • [ ] 内置更多有用的标签
  • [ ] 内置更多有用的过滤器。
  • [ ] pp-html-reverse: 反向解析html字符串为pp-html的S表达式形式。

内置过滤器和标签参考Liquid.

6 鸣谢

pp-html是我写的第一个emacs包。由于是新手,开发过程断断续续持续了一个多月的时间,其间遇到了许多的技术难题。特别感谢 Emacs-China社区 的同学们答疑解惑。

此package可能有不成熟的地方,希望读者诸君、emacs大牛批评指正。关于package功能的拓展和集成,也可以给我提建议(issue或博客留言)。

如果你觉得我的工作对你有所帮助,欢迎 Star 此代码仓库!

发布于 2020-05-10

Emacs Hack - 视频压缩及格式(gif)转换

MacOS系统使用mov作为默认视频格式,mov格式的缺点是文件体积大,不便传输与使用。大多数时候我需要将视频压缩为mp4格式。同时,写文档时经常需要使用gif图片展示hack效果,所以将视频转为gif也成了硬需求之一。关于视频的压缩与格式转换,有很多在线工具以及app可以实现,但我更钟爱命令行工具。因为命令行比封装好的ui使用方便,可选参数多。最重要的是,可以和emacs结合。

ffmpeg 是一个非常优秀的视频处理命令,MacOS下可使用homebrew安装。做了下小hack,以后就可以在emacs中处理视频文件,生成gif啦。

(defun my/video-compress-and-convert (video new)
  (interactive "fvideo path: \nfnew item name (eg. exam.mp4, exam.gif) : ")
  (let ((extension (cadr (split-string (file-name-nondirectory new) "\\."))))
    (if (string= extension "gif")
        (progn
          (shell-command
           (concat "ffmpeg -i " video " -r 5 " new))
          (message "%s convert to %s successfully!" video new))
      (progn
        (shell-command
         (concat "ffmpeg -i " video " -vcodec libx264 -b:v 5000k -minrate 5000k -maxrate 5000k -bufsize 4200k -preset fast -crf 20 -y -acodec libmp3lame -ab 128k " new))
        (message "%s compress and convert to %s successfully!" video new))
      )
    ))

除此之外, ffmpeg 的功能远不止这些,还支持视频的录制、剪辑合并、加滤镜和音频处理等,读者可以访问 ffmpeg官网 探索。

发布于 2020-02-20

Emacs Lisp 编程总结

原文参考: Elisp Programming

1 lisp介绍

Lisp(历史上拼写为LISP)是具有悠久历史的计算机编程语言家族,有独特和完全括号的前缀符号表示法。起源于公元1958年,是现今第二悠久而仍广泛使用的高端编程语言。只有FORTRAN编程语言比它更早一年。Lisp编程语族已经演变出许多种方言。现代最著名的通用编程语种是Clojure、Common Lisp和Scheme。

Lisp最初创建时受到阿隆佐·邱奇的lambda演算的影响,用来作为计算机程序实用的数学表达。因为是早期的高端编程语言之一,它很快成为人工智能研究中最受欢迎的编程语言。在计算机科学领域,Lisp开创了许多先驱概念,包括:树结构、自动存储器管理、动态类型、条件表达式、高端函数、递归、自主(self-hosting)编译器、读取﹣求值﹣输出循环(英语:Read-Eval-Print Loop,REPL)。

"LISP"名称源自“列表处理器”(英语:LISt Processor)的缩写。列表是Lisp的主要数据结构之一,Lisp编程代码也同样由列表组成。因此,Lisp程序可以把源代码当作数据结构进行操作,而使用其中的宏系统,开发人员可将自己定义的新语法或领域专用的语言,嵌入在Lisp编程中。

代码和数据的可互换性为Lisp提供了立即可识别的语法。所有的Lisp程序代码都写为S-表达式或以括号表示的列表。函数调用或语义形式也同样写成列表,首先是函数或操作符的名称,然后接着是一或多个参数:例如,取三个参数的函数f即为(f arg1 arg2 arg3)。

Lisp语言的主要现代版本包括Common Lisp, Scheme,Racket以及Clojure。1980年代盖伊·史提尔二世编写了Common Lisp试图进行标准化,这个标准被大多数解释器和编译器所接受。还有一种是编辑器Emacs所派生出来的Emacs Lisp(而Emacs正是用Lisp作为扩展语言进行功能扩展)非常流行,并创建了自己的标准。

2 Elisp

2.1 概览

2.1.1 运行emacs-lisp的几种方式

key command description
C-x C-e eval-last-sexp 在S表达式结尾运行,在minibuffer显示结果
C-j eval-print-last-sexp 在S表达式结尾运行,打印运行结果
M-: eval-expression 在minibuffer输入命令并执行
M-x ielm ielm 使用IELM解释器运行代码

2.1.2 创建命令(interactive函数)

;; example
(defun buffer/insert-filename ()
  "Insert file path of current buffer at current point"
  (interactive)
  (insert (buffer-file-name (current-buffer))))

2.1.3 emacs探索

key command description
C-h k describe-key 运行命令后,继续按键,查看此时按键绑定的函数
C-h b describe-bindings 在*Help*界面搜索 Major Mode Bindings: 可以查看所有与当前major mode相关的按键。
C-h f describe-function 查看函数文档及详细代码

2.2 elisp编程的基本设置

三个有用的pcakage:

  • rainbow-delimiters: 不同颜色区分不同层级的括号
  • paredit: 检查括号匹配
  • company: elisp代码补全

2.3 基本运算

2.3.1 算术

ELISP> (+ 20 30)
50
ELISP> (- 100 80)
20
ELISP> (+ 1 2 3 4 5 6)
21
ELISP> (* 1 2 3 4 5 6)
720
ELISP> (/ 1 100)
0

ELISP> (> 10 1) ;; ?? 10 > 1
t
ELISP> (< 2 8) ;; ?? 2 < 8
t
ELISP> (< 8 2) ;; ?? 8 < 2
nil

ELISP> (= 2 2)
t
ELISP> (= 2 4)
nil

ELISP> (/= 2 2)
nil
ELISP> (exp -1)
0.36787944117144233
ELISP> (log 10)
2.302585092994046
ELISP> (sin pi)
1.2246467991473532e-16
ELISP> (cos pi)
-1.0
ELISP> (tan (/ pi 2))
1.633123935319537e+16
ELISP>

2.3.2 比较

;;;; Compare Numbers
ELISP> (= 2 (+ 1 1))
t

;;; Compare Symbols and Numbers
ELISP> (eq 1 1)
t
ELISP> (eq 1 2)
nil
ELISP>

ELISP> (eq 'x 'x)
t
ELISP>

;;; Compare Elements of a List
ELISP> (equal (list 1 2 3 4) (list 1 2 3 4))
t

;;; Compare Strings
ELISP> (string= "hello" "hello")
t

2.3.3 列表

ELISP> '(10 20 30 40)
(10 20 30 40)

ELISP> '(10 203 40 "hello" () ("empty" 65))
(10 203 40 "hello" nil
    ("empty" 65))

2.4 类型判断和Literals

2.4.1 Emacs Literals

;;; Numbers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ELISP> 1e3
1000.0

;;; String
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ELISP> "Hello World Emacs Literals"
"Hello World Emacs Literals"
ELISP>

;;; Symbol
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> 'this-a-symbol
this-a-symbol

ELISP> 'vector->list
vector->list

ELISP> 'symbol?
symbol\?
ELISP>

;; Boolean t and nil
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ELISP> t
t
ELISP> nil
nil
ELISP>

 ;;; Everything that is not "nil" is true:
 ;;-----------------------------------------
ELISP> (if t "It is true (not nil)" "It is false (it is nil)")
"It is true (not nil)"
ELISP>
ELISP> (if 100e3 "It is true (not nil)" "It is false (it is nil)")
"It is true (not nil)"
ELISP> (if '(a b c d)  "It is true (not nil)" "It is false (it is nil)")
"It is true (not nil)"
ELISP>

ELISP> (if nil  "It is true (not nil)" "It is false (it is nil)")
"It is false (it is nil)"
ELISP>

;;; Pair / Cons Cell
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ELISP> '(a . b)
(a . b)

ELISP> '(a . 2999)
(a . 2999)

;;; List
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ELISP> '(1 2 3 (3 4) (5 6 (+ 3 4)) 10 'a 'b "hello" )
(1 2 3
   (3 4)
   (5 6
      (+ 3 4))
   10 'a 'b "hello")

ELISP> '(+ 1 2 3 4 5)
(+ 1 2 3 4 5)

ELISP> '(cos 10)
(cos 10)

;;; Vectors
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ELISP> [1 2 3 4 (+ 1 2 3 54)]
[1 2 3 4
   (+ 1 2 3 54)]

2.4.2 基本类型判断

Type Predicate Literal Description
Nil null nil '() Test if argument is nil
Numbers numberp 100, 200e3 Test if it is number.
String stringp "hello" Test if it is string
Symbol symbolp 'sym :keyworkd Test if it is a symbol.
       
Atom atom 'x "h" :key 200 Everything that is not a list or pair is an atom.
List listp '(1 2 x y) Test if it is a list
Pair consp '(a . 200) Test if it is a pair (cons cell)
Vector vectorp [1 200 'sym] Test if it is a vector
Object Predicate
Buffer bufferp
Window windowp
Frame framep
Process processp
ELISP> (null nil)
t
ELISP>
ELISP> (null '())
t

ELISP> (null 10)
nil

ELISP> (atom 10)
t
ELISP> (atom '(a . b))
nil
ELISP> (atom "hello world")
t
ELISP>

ELISP> (bufferp (current-buffer))
t
ELISP> (bufferp (selected-window))
nil
ELISP> (windowp (selected-window))
t
ELISP>

2.4.3 获取对象类型

ELISP> (type-of (current-buffer))
buffer
ELISP>
ELISP> (type-of (selected-window))
window
ELISP>

ELISP> (equal 'buffer (type-of (current-buffer)))
t
ELISP> (equal 'buffer (type-of (selected-window)))
nil
ELISP>

2.5 变量定义

;;; Constants
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (defconst zsh-shell "/usr/bin/zsh")
zsh-shell

ELISP> zsh-shell
"/usr/bin/zsh"
ELISP>

;;; Define a variable
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;; Set is not used very much
;;
ELISP> (set 'avar "hello world")
"hello world"

ELISP> avar
"hello world"
ELISP>

;;;;; The most used command for assignment is setq
;;
ELISP> (setq x 10)
10

ELISP> (setq avar "hello world")
"hello world"

ELISP> x
10

ELISP> avar
"hello world"
ELISP>

ELISP> (setq my-list '(10 20 30 40))
(10 20 30 40)

ELISP> my-list
(10 20 30 40)

;;; Multiple Assignment
;;
ELISP> (setq a 10 b 20 c "Emacs")
"Emacs"
ELISP> a
10
ELISP> b
20
ELISP> c
"Emacs"
ELISP>

;; Dynamic Scoping  (Local Variables)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
ELISP> (let ((x 1) (y 10)) (+ (* 4 x) (* 5 y)) )
54
ELISP> x
    ** Eval error **  Symbol's value as variable is void: x
ELISP> y
    ** Eval error **  Symbol's value as variable is void: y
ELISP>

2.6 函数定义

2.6.1 定义简单函数

语法: (defun <function name> (<parameters>) (<body>))

ELISP> (defun afunction (a b c) (+ a b c))
afunction

ELISP> (afunction 10 20 30)
60

ELISP> (defun myfun () (message "Hello Emacs"))
myfun
ELISP> (myfun)
"Hello Emacs"
ELISP>


ELISP>
ELISP> (defun signum (n)
     (cond ((> n 0) 1 )
       ((< n 0) -1)
       (0)))
signum
ELISP> (signum 10)
1
ELISP> (signum 0)
0
ELISP> (signum -23)
-1
ELISP>


ELISP> (defun factorial (n)
     (if (= n 0)
     1
     (* n (factorial (- n 1)))))
factorial

ELISP> (factorial 5)
120
ELISP

2.6.2 匿名函数/Lambda函数

语法: (lambda (<parameters>) (<body>))

ELISP> (lambda (x) (+ x 3))
(lambda
  (x)
  (+ x 3))

;;; Applying Lambda Functions
;;

ELISP> ((lambda (x) (+ x 3)) 4)
7
ELISP> (funcall (lambda (x) (+ x 3)) 4)
7
ELISP>

;;; Storing Lambda Function in Variable
;;
;;

ELISP> (defvar add3 (lambda (x) (+ x 3)))
add3


ELISP> add3
(lambda
  (x)
  (+ x 3))

ELISP> (funcall add3 10)
13

ELISP> (add3 10)
    ** Eval error **  Symbol's function definition is void: add3

ELISP> (funcall #'add3 10)
    ** Eval error **  Symbol's function definition is void: add3
ELISP>

;;; Passing Lambda Function to functions
;;
ELISP> (mapcar (lambda (x) (+ x 3))  '(1 2 3 4 5))
(4 5 6 7 8)

2.6.3 函数作为参数

语法: (caller-function #'<function-1> #'<function-1> arg1 arg2 …)

在函数内部,使用 funcall 调用函数作为参数

ELISP> (mapcar log '(1 10 100 1000))
    ** Eval error **  Symbol's value as variable is void: log


ELISP> (mapcar #'log10 '(1 10 100 1000))
(0.0 1.0 2.0 3.0)

(defun sum-fun (f1 f2 x)
  (+ (funcall f1 x) (funcall f2 x)))

ELISP> (sum-fun #'log #'exp 3)
21.18414921185578
ELISP>

ELISP> (+ (log 3) (exp 3))
21.18414921185578
ELISP>

ELISP> (sum-fun (lambda (x) (* 3 x))
    (lambda (x) (* 4 x))
    5)
35
ELISP>

ELISP> (defun 1+ (x) (+ 1 x))
1+
ELISP> (defun 3* (x) (* 3 x))
3*

ELISP> (sum-fun #'1+  #'3* 4)
17
ELISP>

ELISP> (sum-fun #'1+  (lambda (x) (* 3 x)) 4)
17
ELISP>

2.6.4 多参函数

(defun sum (&rest numbers)
  (apply #'+ numbers))

ELISP> (sum 1 2 3 4 5 6)
21


ELISP> (apply #'sum '(1 2 3 5 6))
17

ELISP> (apply #'sum (list 1 2 3 5 (+ 6 5 2)))
24

ELISP> (apply #'sum '())
0

ELISP> (apply #'sum nil)
0

ELISP> (sum nil)
    ** Eval error **  Wrong type argument: number-or-marker-p, ni

;;----------------------------------

(defun sum-prod (a &rest xs)
  (* a (apply #'+ xs)))


ELISP> (sum-prod 3 1 2 3 4 5)
45

ELISP> (sum-prod 1 1 2 3 4 5)
15

2.6.5 可选参数函数

(defun test-optional (a &optional b)
  (list a b))

ELISP> (test-optional 10 20)
(10 20)

ELISP> (test-optional 10 )
(10 nil)

;--------------------------------;

(defun test-optional2 (a b &optional b c d e)
  (list :a a :b b :c c :d d :e e))

ELISP> (test-optional2 0 1 2 3 4 5 )
(:a 0 :b 2 :c 3 :d 4 :e 5)


ELISP> (test-optional2 0 1 2 3 4  )
(:a 0 :b 2 :c 3 :d 4 :e nil)

ELISP> (test-optional2 0 1 2 3   )
(:a 0 :b 2 :c 3 :d nil :e nil)

ELISP> (test-optional2 0 1 2    )
(:a 0 :b 2 :c nil :d nil :e nil)

ELISP> (test-optional2 0 1  )
(:a 0 :b nil :c nil :d nil :e nil)

ELISP> (test-optional2 0 1)
(:a 0 :b nil :c nil :d nil :e nil)

;--------------------------------;

(defun test-optional-default-b (a &optional b)
  (if b
      (list a b)
      (list a "b is null")))

ELISP> (test-optional-default-b 1 2)
(1 2)

ELISP> (test-optional-default-b 1)
(1 "b is null")

ELISP> (test-optional-default-b 1 nil)
(1 "b is null")

2.6.6 含属性列表参数函数

(defun make-shell-interface (&rest params)
  "
  Create a shell interface.

  Possible parameters:

    :name      Name of shell
    :type      ['sh, 'bash, ...]
    :path      Path to program
    :buffer    Name of buffer

  "
  (let
       ((name   (plist-get params :name ))
    (type   (plist-get params :type))
    (path   (plist-get params :path))
    (buffer (plist-get params :buffer)))
    (list
     (cons 'name buffer)
     (cons 'type type)
     (cons 'path path)
     (cons 'buffer buffer))))


ELISP> (make-shell-interface :name "pylaucher" :path "/usr/bin/python" :type 'sh :buffer "pyshell")
((name . "pyshell")
 (type . sh)
 (path . "/usr/bin/python")
 (buffer . "pyshell"))

ELISP> (make-shell-interface :name "pylaucher" :path "/usr/bin/python" :type 'sh)
((name)
 (type . sh)
 (path . "/usr/bin/python")
 (buffer))

ELISP> (make-shell-interface :name "pylaucher" :path "/usr/bin/python" :type 'bash)
((name)
 (type . bash)
 (path . "/usr/bin/python")
 (buffer))

ELISP> (make-shell-interface :name "pylaucher" :path "/usr/bin/python")
((name)
 (type)
 (path . "/usr/bin/python")
 (buffer))

ELISP> (make-shell-interface :name "pylaucher" )
((name)
 (type)
 (path)
 (buffer))

ELISP> (make-shell-interface  )
((name)
 (type)
 (path)
 (buffer))

ELISP> (make-shell-interface :buffer "pyshell"  :path "/usr/bin/python" :type 'sh :name "pylaucher")
((name . "pyshell")
 (type . sh)
 (path . "/usr/bin/python")
 (buffer . "pyshell"))

2.6.7 Closures

elisp方言默认不支持closure,所以下面的代码不会像Scheme或Common Lisp一样执行。

参考:

(defun make-adder (x)
  (lambda (y) (+ x y)))


ELISP>
ELISP> (make-adder 3)
(lambda
  (y)
  (+ x y))

ELISP> ((make-adder 3) 4)
    ** Eval error **  Invalid function: (make-adder 3)
ELISP> (funcall (make-adder 3) 4)
    ** Eval error **  Symbol's value as variable is void: x
ELISP> (map (make-adder 3) '(1 2 3 4 5))
    ** Eval error **  Symbol's value as variable is void: x
ELISP>

支持closure的代码:

(setq lexical-binding t)

(defun make-adder (x)
  (lambda (y) (+ x y)))

ELISP> (make-adder 3)
(closure
 ((x . 3)
  t)
 (y)
 (+ x y))

ELISP> ((make-adder 3) 4)
    ** Eval error **  Invalid function: (make-adder 3)
ELISP>

ELISP> (funcall (make-adder 3) 4)
7
ELISP>

ELISP> (mapcar (make-adder 3) '(1 2 3 4 5))
(4 5 6 7 8)


;;;; Sometimes is better to create macro rather than a higher order function


(defmacro make-sum-fun (f1 f2)
  `(lambda (x) (+ (,f1 x) (,f2 x))))

ELISP>
ELISP> (funcall (make-sum-fun sin cos) 3)
-0.8488724885405782
ELISP>
ELISP> (make-sum-fun sin cos)
(closure
 (t)
 (x)
 (+
  (sin x)
  (cos x)))

ELISP> (map (make-sum-fun sin cos) '(1 2 3 4 5))
(1.3817732906760363 0.4931505902785393 -0.8488724885405782 -1.4104461161715403 -0.6752620891999122)

~/.emacs.d/init.el 中添加如下配置以支持closure.

(setq lexical-binding t)

2.7 列表操作

参考:

;; Defining a List
;;
;; An emacs list can contain elements of almost any type.
;;
ELISP> '( "a" 2323 "b" 21.2323 "hello" "emacs" nil () (34 134) '(+ 2 3 5))
("a" 2323 "b" 21.2323 "hello" "emacs" nil nil
 (34 134)
 '(+ 2 3 5))

ELISP> (quote (1 3 3 4 5))
(1 3 3 4 5)

;;;;; Empty List
;;
ELISP> nil
nil
ELISP> '()
nil
ELISP>

;; Length of a list
ELISP> (length '(1 2 3 4 5 6))
6
ELISP>


;; nth element of a list
;;
ELISP> (nth 0 '(0 1 2 3 4 5))
0
ELISP> (nth 2 '(0 1 2 3 4 5))
2
ELISP> (nth 5 '(0 1 2 3 4 5))
5
ELISP> (nth 10 '(0 1 2 3 4 5))
nil
ELISP>


;; Membership test
;; member returns null if the element is not member of the list
;;
ELISP> (member 2 '(0 1 2 3 4 5))
(2 3 4 5)

ELISP> (member 10 '(0 1 2 3 4 5))
nil
ELISP>

;; Position of list element (prior to emacs 24.4)
;;
ELISP> (position 7 '(5 6 7 8))
2

ELISP> (position 17 '(5 6 7 8))
nil
ELISP>

;; Position of list element (emacs 24.4 or later)
;;
ELISP> (cl-position 7 '(5 6 7 8))
2

ELISP> (cl-position 17 '(5 6 7 8))
nil
ELISP>

;; cdr
;;
;; Removes first element of the list, returns the list tail.
;;
ELISP> (cdr '(1 2 3 4 5))
(2 3 4 5)

;; car
;;
;; Returns the first list element
;;
ELISP> (car '(1 2 3 4 5))
1
ELISP>


;; cons
;;
;; List constructor
;;
ELISP> (cons 10 '(1 2 3 4))
(10 1 2 3 4)

ELISP> (cons 1 (cons 2 (cons 3 (cons 4 (cons 5 '())))))
(1 2 3 4 5)

;; Last element of a list
;;
;;
ELISP> (car (last '(1 2 3 4 5)))
5
ELISP>


;; Reverse a list
;;
ELISP> (reverse '(1 2 3 4 5))
(5 4 3 2 1)


;; Append lists
;;
;; Note: nil also means an empty list
;;
ELISP> (append '(1 2) '( "a" "b" "c" "d"))
(1 2 "a" "b" "c" "d")

ELISP> (append '(1 2) nil '( "a" "b" "c" "d") nil)
(1 2 "a" "b" "c" "d")



;; Filter list elements given a predicate function
;;
;;
ELISP> (remove-if-not (lambda (x) (> x 2)) '(1 2 3 4 5 6 7 8 9 10))
(3 4 5 6 7 8 9 10)

;; Test if list is empty
;;
ELISP> (null '(1 2 3 4 5))
nil
ELISP> (null '())
t
ELISP> (null nil)
t
ELISP>

;; Drop the firsts n elements of a list
;;
;;
ELISP> (nthcdr 2 '(1 2 3 4))
(3 4)

ELISP> (nthcdr 3 '(1 2 3 4))
(4)

ELISP> (nthcdr 13 '(1 2 3 4))
nil
ELISP>

;; Delete an element of a list
;;
;;
ELISP> (delq 1 '(1 2 3 4))
(2 3 4)


ELISP> (delq 10 '(1 2 3 4))
(1 2 3 4)

;; It doesn't work to delete sublists
;;
ELISP> (delq (5) '(1 2 (5) 3 4))
    ** Eval error **  Invalid function: 5
ELISP> (delq '(5) '(1 2 (5) 3 4))
(1 2
   (5)
   3 4)

ELISP> (delete '(5) '(1 2 (5) 3 4))
(1 2 3 4)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; Convert Vector to List
;;
;;
ELISP> (coerce [1 2 3] 'list)
(1 2 3)

;; Convert List to Vector
;;
ELISP> (coerce '(1 2 3) 'vector)
[1 2 3]

ELISP> (number-sequence 0 10 2)
(0 2 4 6 8 10)

ELISP> (number-sequence 9 4 -1)
(9 8 7 6 5 4)


;; Modify list variables.
;;
ELISP> alist
(a b c d e)

ELISP> (push 'f alist)
(f a b c d e)

ELISP> alist
(f a b c d e)

ELISP> (pop alist)
f

ELISP> alist
(a b c d e)

ELISP> (pop alist)
a
ELISP> alist
(b c d e)

ELISP>

2.8 关联列表和属性列表

2.8.1 概览

关联列表是一系列cons对,这里我可以称作 clist 或者 由两个元素组成的列表的集合,可以称为 alist

关联列表类型:clist

键: a, x, 2 and 4 值: b, y, 3 and (1 2 3 4 5)

ELISP> '((a . b) (x . y) (2 . 3) (4 . (1 2 3 4 5)))
((a . b)
 (x . y)
 (2 . 3)
 (4 1 2 3 4 5)

ELISP> (cons 'a 'b)
(a . b)

ELISP> (cons 'a (cons 'b (cons 'c nil)))
(a b c)

关联列表类型:alist

ELISP> '((a  b) (x  y) (2  3) (4  (1 2 3 4 5)))
((a b)
 (x y)
 (2 3)
 (4
  (1 2 3 4 5)))

ELISP> (list (list 'a 'b) (list 'x 'y) (list 2 3) (list 2 '(1 2 3 4 5)))
((a b)
 (x y)
 (2 3)
 (2
  (1 2 3 4 5)))

alist 不像 clist 有歧义。

属性列表:Plist

属性列表是连续的键值对集合,它的优势是括号少和可读性高。

'(:key1 value1 :key2 value2 :key3 1002.23 :key4 (a b c d e))

ELISP> '(:key1 value1 :key2 value2 :key3 1002.23 :key4 (a b c d e))
(:key1 value1 :key2 value2 :key3 1002.23 :key4
       (a b c d e))

;;; It is more useful in configuration files

(
:key1  value1
:key2  value2
:key3  value3
:key4  (a b c d e )
)

2.8.2 关联列表/Alist

ELISP> (setq dict
'((pine . cones)
 (oak . acorns)
 (maple . seeds)))
((pine . cones)
 (oak . acorns)
 (maple . seeds))

ELISP> dict
((pine . cones)
 (oak . acorns)
 (maple . seeds))

;; Get a cell associated with a key
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ELISP>
ELISP> (assoc 'oak dict)
(oak . acorns)

ELISP> (assoc 'wrong dict)
nil

;; Get a Key
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (car (assoc 'oak dict))
oak
ELISP> (cdr (assoc 'oak dict))
acorns
ELISP>


ELISP> (car (assoc 'oak dict))
oak
ELISP>

;; Get all keys
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (mapcar #'car dict)
(pine oak maple)

;; Get all values
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (mapcar #'cdr dict)
(cones acorns seeds)

例:过滤多个键

ELISP> (defvar language-list
  '(
   ("io" . ((:command . "io")
         (:description . "Run IO Language script")))
    ("lua" . ((:command . "lua")
          (:description . "Run Lua script")))
    ("groovy" . ((:command . "groovy")
         (:description . "Run Groovy")))
    ("scala" . ((:command . "scala")
        (:cmdopt . "-Dfile.encoding=UTF-8")
        (:description . "Run Scala file with scala command")))

    ("haml" . ((:command . "haml")
           (:exec    . "%c %o %s")
           (:description . "Convert HAML to HTML")))
    ("sass" . ((:command . "sass")
           (:exec    . "%c %o --no-cac")))
 ))
language-list


ELISP> (assoc  "scala"  language-list )
("scala"
 (:command . "scala")
 (:cmdopt . "-Dfile.encoding=UTF-8")
 (:description . "Run Scala file with scala command"))

ELISP> (assoc  "lua"  language-list )
("lua"
 (:command . "lua")
 (:description . "Run Lua script"))

ELISP> (assoc  "wrong"  language-list )
nil

ELISP> (assoc ':command (assoc  "scala"  language-list ))
(:command . "scala")

ELISP> (cdr (assoc ':command (assoc  "scala"  language-list )))
"scala"
ELISP>

ELISP> (assoc ':description (assoc  "scala"  language-list ))
(:description . "Run Scala file with scala command")

ELISP> (cdr (assoc ':description (assoc  "scala"  language-list )))
"Run Scala file with scala command"
ELISP>

ELISP> (mapcar 'car language-list)
("io" "lua" "groovy" "scala" "haml" "sass")

ELISP> (mapcar 'cdr language-list)
(((:command . "io")
  (:description . "Run IO Language script"))
 ((:command . "lua")
  (:description . "Run Lua script"))
 ((:command . "groovy")
  (:description . "Run Groovy"))
 ((:command . "scala")
  (:cmdopt . "-Dfile.encoding=UTF-8")
  (:description . "Run Scala file with scala command"))
 ((:command . "haml")
  (:exec . "%c %o %s")
  (:description . "Convert HAML to HTML"))
 ((:command . "sass")
  (:exec . "%c %o --no-cac")))

ELISP>

ELISP> (mapcar (lambda (x) (
                 list
                 (car x)
                 (cdr x)
                 ))
                language-list)
(("io"
  ((:command . "io")
   (:description . "Run IO Language script")))
 ("lua"
  ((:command . "lua")
   (:description . "Run Lua script")))
 ("groovy"
  ((:command . "groovy")
   (:description . "Run Groovy")))
 ("scala"
  ((:command . "scala")
   (:cmdopt . "-Dfile.encoding=UTF-8")
   (:description . "Run Scala file with scala command")))
 ("haml"
  ((:command . "haml")
   (:exec . "%c %o %s")
   (:description . "Convert HAML to HTML")))
 ("sass"
  ((:command . "sass")
   (:exec . "%c %o --no-cac"))))

ELISP>

ELISP> (mapcar (lambda (x) (
     list
     (car x)
     (assoc ':command       (cdr x))
     (assoc ':cmdopt        (cdr x))
     (assoc ':description   (cdr x))
     ))
    language-list)

(("io"
  (:command . "io")
  nil
  (:description . "Run IO Language script"))
 ("lua"
  (:command . "lua")
  nil
  (:description . "Run Lua script"))
 ("groovy"
  (:command . "groovy")
  nil
  (:description . "Run Groovy"))
 ("scala"
  (:command . "scala")
  (:cmdopt . "-Dfile.encoding=UTF-8")
  (:description . "Run Scala file with scala command"))
 ("haml"
  (:command . "haml")
  nil
  (:description . "Convert HAML to HTML"))
 ("sass"
  (:command . "sass")
  nil nil))

ELISP>


ELISP> (mapcar (lambda (x) (
     list
     (car x)
     (cdr (assoc ':command   (cdr x)))
     (cdr (assoc ':cmdopt       (cdr x)))
     (cdr (assoc ':description   (cdr x)))
     ))

    language-list)
(("io" "io" nil "Run IO Language script")
 ("lua" "lua" nil "Run Lua script")
 ("groovy" "groovy" nil "Run Groovy")
 ("scala" "scala" "-Dfile.encoding=UTF-8" "Run Scala file with scala command")
 ("haml" "haml" nil "Convert HAML to HTML")
 ("sass" "sass" nil nil))

ELISP>

ELISP> (defun get-value (alist key) (cdr (assoc key alist)))
get-value
ELISP> (get-value language-list "scala")
((:command . "scala")
 (:cmdopt . "-Dfile.encoding=UTF-8")
 (:description . "Run Scala file with scala command"))

ELISP> (get-value language-list "lua")
((:command . "lua")
 (:description . "Run Lua script"))

ELISP>
ELISP> (get-value language-list "0")
nil
ELISP>


ELISP> (defun get-key-value (alist key field)
        (cdr (assoc  field  (cdr (assoc key alist))  )))
get-key-value
ELISP>
ELISP> (get-key-value language-list "scala" ':description)
"Run Scala file with scala command"
ELISP>

ELISP> (get-key-value language-list "scala" ':command)
"scala"
ELISP>

2.8.3 属性列表

ELISP> (defvar plst (list :buffer (current-buffer) :line 10 :pos 2000))
plst

ELISP>
ELISP> (plist-get plst :line)
10

ELISP> (plist-get plst :pos)
2000

ELISP> (plist-get plst :buffer)
#<buffer *ielm*>
ELISP>

ELISP>
ELISP> (plist-get plst :buffdfds)
nil
ELISP>

ELISP> (plist-member plst :buffer)
(:buffer #<buffer *ielm*> :line 10 :pos 2000)

ELISP> (plist-member plst :bufferasd)
nil
ELISP>

ELISP> (plist-put plst :winconf (current-window-configuration))
(:buffer #<buffer *ielm*> :line 10 :pos 2000 :winconf #<window-configuration>)

ELISP> plst
(:buffer #<buffer *ielm*> :line 10 :pos 2000 :winconf #<window-configuration>)

ELISP>

2.8.4 转换Alist成Plist和vice-versa

;; Alist to plist
(defun plist->alist (plist)
  (if (null plist)
      '()
      (cons
       (list (car plist) (cadr plist))
       (plist->alist (cddr plist)))))

ELISP> (plist->alist (list :x 10 :y 20 :name "point"))
((:x 10)
 (:y 20)
 (:name "point"))

;;; Converts association list to plist
(defun alist->plist (assocl)
  (if (null assocl)
      '()
    (let
    ((hd (car assocl))
     (tl (cdr assocl)))
      (cons (car hd)
    (cons (cadr hd)
      (alist->plist tl))))))

;;; Converts plist to clist (List of cons pairs)
(defun plist->clist (plist)
  (if (null plist)
      '()
      (cons
       (cons (car plist) (cadr plist))
      (plist->clist (cddr plist)))))

ELISP> (plist->clist (list :x 10 :y 20 :name "point"))
((:x . 10)
 (:y . 20)
 (:name . "point"))

;; Separates a property list into two lists of keys and values.
;;
(defun plist->kv (plist)
  (let ((alist (plist->alist plist)))
    (cons
     (mapcar #'car alist)
     (mapcar #'cdr alist))))

ELISP> (setq al (plist->alist (list :x 10 :y 20 :name "point")))
((:x 10)
 (:y 20)
 (:name "point"))

ELISP> (alist->plist al)
(:x 10 :y 20 :name "point")

ELISP>

(setq keylist
    '("M-i"  'previous-line
      "M-j"  'backward-char
      "M-k"  'next-line
      "M-l"  'forward-char))


ELISP> (setq kv (plist->kv keylist))
(("M-i" "M-j" "M-k" "M-l")
 ('previous-line)
 ('backward-char)
 ('next-line)
 ('forward-char))

ELISP> (car kv)
("M-i" "M-j" "M-k" "M-l")

ELISP> (cdr kv)
(('previous-line)
 ('backward-char)
 ('next-line)
 ('forward-char))

ELISP>

2.9 字符串

;; Split String

ELISP> (split-string "  two words ")
("two" "words")

ELISP>

ELISP> (split-string "o\no\no" "\n" t)
("o" "o" "o")

ELISP> (split-string "Soup is good food" "o*" t)
("S" "u" "p" " " "i" "s" " " "g" "d" " " "f" "d")

ELISP>

;; Format String

ELISP> (format-time-string "%Y/%m/%d %H:%M:%S" (current-time))
"2015/06/26 06:10:04"
ELISP>
ELISP>


;; Concatenate Strings

ELISP> (concat "The " "quick brown " "fox.")
"The quick brown fox."
ELISP>

ELISP> (mapconcat 'identity '("aaa" "bbb" "ccc") ",")
"aaa,bbb,ccc"
ELISP> (split-string "aaa,bbb,ccc" ",")
ELISP> (split-string "aaa,bbb,ccc" ",")
("aaa" "bbb" "ccc")

;; String Width

ELISP> (string-width "hello world")
11
ELISP>
ELISP> (substring "Freedom Land" 0 5)
"Freed"
ELISP>
ELISP> (string-match "ce" "central park")
0
ELISP> (string-match "gt" "central park")
nil
ELISP>


;;;;; Misc

ELISP> (make-string 5 ?x)
"xxxxx"
ELISP> (make-string 5 ?a)
"aaaaa"
ELISP> (make-string 5 ?r)
"rrrrr"
ELISP> (make-string 15 ?r)
"rrrrrrrrrrrrrrr"
ELISP>

elisp符号/字符串转换

; Convert a symbol to string
ELISP> (symbol-name 'wombat)
"wombat"

; Convert a String to Symbol
ELISP> (intern "wombat")
wombat

读取字符串中的S表达式

ELISP> (read-from-string
        "(
           (POINT1  (X  10.2323)  (Y   20.2323))
           (POINT2  (x  0.2)          (Y 923.23))
           (POINT3  (x -10.5)       (Y 78,23))
         )")
(((POINT1
   (X 10.2323)
   (Y 20.2323))
  (POINT2
   (x 0.2)
   (Y 923.23))
  (POINT3
   (x -10.5)
   (Y 78
      (\, 23))))
 . 174)

ELISP>

2.10 符号

;;; Convert a string to symbol

ELISP> (intern "a-symbol")
a-synmbol
ELISP> (symbolp (intern "a-symbol"))
t
ELISP>

;;; Convert a symbol to a string

ELISP> (symbol-name 'symbol)
"symbol"
ELISP>

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (setq sym '(1 2 3 4 5))
(1 2 3 4 5)

ELISP> sym
(1 2 3 4 5)

;;; Test if variable is defined
ELISP> (boundp 'sym)
t
ELISP>

;;; Test if variable sym is a symbol
ELISP> (symbolp sym)
nil

;;; Test if the symbol sym is a symbol
ELISP> (symbolp 'sym)
t
ELISP>

;; Get symbol as string
;;
ELISP> (symbol-name 'sym)
"sym"

;; Get value from a symbol
;;
ELISP> (symbol-value 'sym)
(1 2 3 4 5)

ELISP> (symbol-function 'sym)
nil

ELISP> (symbol-plist 'sym)
nil

;;-------------------------;;

ELISP> (defun func (x y) (+ (* 3 x) (* 4 y)))
func

ELISP> (func 10 2)
38
ELISP>

;;; Check if function is defined
;;
ELISP> (fboundp 'func)
t
ELISP> (fboundp 'sym)
nil
ELISP>


ELISP> (symbol-name 'func)
"func"

ELISP> (symbol-value 'func)
    ** Eval error **  Symbol's value as variable is void: func
ELISP> (symbol-function 'func)
(lambda
  (x y)
  (+
   (* 3 x)
   (* 4 y)))

ELISP> (symbol-plist 'func)
nil
ELISP>

;;; Function Source Code

ELISP> (symbol-function #'func)
(lambda
  (x y)
  (+
   (* 3 x)
   (* 4 y)))


;; Test if function is an elisp primitive

ELISP> (subrp (symbol-function 'goto-char))
t
ELISP>

2.11 类型转换

类型查询

ELISP> (type-of 1000)
integer

ELISP> (type-of 1000.3434)
float
ELISP>

ELISP> (type-of "lisp")
string

ELISP> (type-of '(1 2 3 4 5))
cons
ELISP> (type-of (list 'cos 'sin 1 2 3 4 5))
cons
ELISP>

ELISP> (type-of [1 2 3 4])
vector

ELISP> (type-of 'elisp-mode-map)
symbol
ELISP>

ELISP> (type-of #'cos)
symbol
ELISP>

类型判断

;; Test if it is a number
;;-----------------------------------

ELISP> (numberp 1000)
t
ELISP> (numberp 10e4)
t
ELISP> (numberp '(1 2 3 4))
nil
ELISP> (numberp "hello world")
nil
ELISP>


;; Test if it is a string
;;-----------------------------------

ELISP> (stringp "Emacs")
t
ELISP> (stringp '(1 2 3 4))
nil
ELISP>

;; Test if ti is a symbol
;;------------------------------------
ELISP> (symbolp 'emacs)
t
ELISP> (symbolp #'emacs)
t
ELISP> (symbolp "something")
nil
ELISP> (symbolp 10000)
nil
ELISP>


;; Test if it is a list
;;-----------------------------------

ELISP> (listp '(1 2 3 4))
t
ELISP> (listp [1 2 3 4])
nil
ELISP> (listp "hello world")
nil
ELISP>


;; Test if it is a vector
;;-----------------------------------

ELISP> (vectorp ["Lisp" "Emacs" "Scheme" "Clojure"])
t
ELISP>
ELISP> (vectorp '(1 2 3))
nil
ELISP> (vectorp "lisp")
nil
ELISP>

数字/字符串转换

ELISP>
ELISP> (number-to-string 1000)
"1000"

ELISP> (string-to-number "200")
200
ELISP>
ELISP>

符号/字符串转换

ELISP> (symbol-name 'my-symbol)
"my-symbol"

ELISP> (symbol-name :my-symbol)
":my-symbol"
ELISP>

ELISP> (intern "some-symbol")
some-symbol

S表达式/字符串转换

  • read: 解析S表达式
ELISP>
ELISP> (setq raw "(:x 10 :y 20 :z 30 :w \"hello world\")")
"(:x 10 :y 20 :z 30 :w \"hello world\")"
ELISP>
ELISP> (read raw)
(:x 10 :y 20 :z 30 :w "hello world")

ELISP> (plist-get (read raw) :x)
10
ELISP> (plist-get (read raw) :w)
"hello world"
ELISP>
  • prin1-to-string: 序列化S表达式
ELISP> (setq sexp '(:x 10 :y 20 :z 30 :w "hello world"))
(:x 10 :y 20 :z 30 :w "hello world")

ELISP> sexp
(:x 10 :y 20 :z 30 :w "hello world")

ELISP> (prin1-to-string sexp)
"(:x 10 :y 20 :z 30 :w \"hello world\")"
ELISP>

2.12 求值

S表达式求值

ELISP> (eval '(+ 1 2 3 4 5))
15
ELISP>


ELISP> '(defun func1(x)(* 10 x))
(defun func1
    (x)
  (* 10 x))

ELISP>

ELISP> '((+ 1 3) (* 4 5) (- 8 9))
((+ 1 3)
 (* 4 5)
 (- 8 9))

ELISP> (eval '(defun func1(x)(* 10 x)))
func1
ELISP> (func1 5)
50
ELISP>


ELISP> (mapcar 'eval '((+ 1 3) (* 4 5) (- 8 9)))
(4 20 -1)

字符串求值

ELISP> (defun eval-string (str) (eval (read str)))
eval-string

ELISP> (eval-string "(+ 1 2 3 4 5 6)")
21
ELISP>

ELISP> (eval-string "(defun func2(x)(* 10 x)))")
func2
ELISP> (func2 6)
60
ELISP>

S表达式格式化为字符串

ELISP> (setq sexp1 '(+ 1 (* 2 3)))
(+ 1
   (* 2 3))

ELISP> (eval sexp1)
7

ELISP> (format "%S" sexp1)
"(+ 1 (* 2 3))"
ELISP>

Elisp中的求值命令

命令 功能
M-x eval-defun 函数求值
M-x eval-region 区域内表达式求值
M-x eval-buffer buffer内表达式求值
M-x eval-expression 输入框输入求值
M-x load-file 文件加载

2.13 Defalias

内置宏 defalias 可以为emaca函数定义简短的名字。

参考:Emacs: Use Alias for Fast M-x

ELISP> (require 'cl)
cl
ELISP>

ELISP> (defalias 'map 'mapcar)
map
ELISP> (map (lambda (x) (* 3 x)) (list 1 2 3 4 5 6))
(3 6 9 12 15 18)

ELISP> (defalias 'filter 'remove-if-not) ;; remove-if-not comes from "cl" library
filter

;;; Filter all buffers bounded to a file
;;
ELISP> (filter #'buffer-file-name (buffer-list))
(#<buffer README.org> #<buffer Projects.wiki.org> #<buffer Index.wiki.org> #<buffer settings.org> #<buffer project.org>)

;;; Reject all buffers which are not bounded to a file
ELISP> (reject #'buffer-file-name (buffer-list))
(#<buffer *ielm*> #<buffer *Help*> #<buffer  *Minibuf-1*> #<buffer emacs> #<buffer *scratch*> ..)

;;; The command M-x org-html-export-to-htm will export this document (README.org) to html
;;  the command M-x org2html will do so too.
;;
(defalias #'org2html #'org-html-export-to-html)

;;
;;  It is also useful to create more convenient names for Emacs API
;; in a namsepace-like fashion that makes easier to find functions and
;; autocomplete functions, for instance:
;;
(defalias 'file/extension         'file-name-extension)
(defalias 'file/extension-sans    'file-name-sans-extension)
(defalias 'file/path-expand       'expand-file-name)
(defalias 'file/filename          'file-name-nondirectory)
(defalias 'file/path-relative     'file-relative-name)
(defalias 'file/rename            'rename-file)
(defalias 'file/delete            'delete-file)
(defalias 'file/copy              'copy-file)

;;; To find the documentation of a function group defined in this fashion
;; Enter M-x apropos  and then type file/
(apropos "file/")

ELISP> (set-buffer "README.org")
#<buffer README.org>
ELISP> (buffer-file-name)
"/home/tux/PycharmProjects/emacs/README.org"
ELISP> (file/basename (buffer-file-name))
"README"
ELISP> (file/extension (buffer-file-name))
"org"
ELISP> (file/filename (buffer-file-name))
"README.org"
ELISP>

2.14 控制结构

2.14.1 Conditional Statement

If Else 语句

;;
;; Any value different from nil or '() is true, otherwise false.
;;

;; True
;;
ELISP> (if t 5 6)
5

ELISP> (if 10 5 6)
5

ELISP> (if 0 5 6)
5

;; False
ELISP> (if nil 5 6)
6

ELISP> (if '() 5 6)
6


;; Inverting Predicate
;;
ELISP> (if (not t) 5 6)
6

ELISP> (if (not nil) 5 6)
5


ELISP> (if (< 5 10)  (message "less than 10") (message "greater or equal to 10") )
"less than 10"

ELISP> (if (< 30 10)  (message "less than 10") (message "greater or equal to 10") )
"greater or equal to 10"
ELISP>

;;; If else with multiple statements

ELISP> (setq x 10)
10

ELISP> (if (> x 5)
       ;; Then Statement
       (progn

     (message "Positive Number")
     (print "Greater than five")
     (split-window-vertically)
     78 ;;  Return Value
    )
     ;; Else Statement
     (progn
       (print "Less than five")
       (split-window-horizontally)
       12 ;;  Return Value
     ))

"Greater than five"

78
ELISP>

When语句

ELISP> (when t 3)
3

ELISP> (when nil 3)
nil


ELISP> (setq x 5)
5

ELISP> (when (> x 3)
     (message "Less than 3"))
"Less than 3"
ELISP>

ELISP> (setq x 1)
1

ELISP> (when (> x 3)
     (message "Less than 3"))
nil
ELISP>


;;;;; When with Multiple Statements

ELISP> (setq x 10)
10

ELISP> (when (> x 7)
     (progn
       (message "Greater than 7 OK.")
       (message "Print message 2")
       (split-window-horizontally)
      ))

 #<window 8 on *ielm*>
ELISP>

2.14.2 Cond - Case Switch

ELISP> (setq a 3)       ;; a = 3
3
ELISP>

ELISP> (cond
    ((evenp a) a)       ;; if   (a % 2 == 0)    ==> a
    ((> a 7) (/ a 2))   ;; elif (a > 7)         ==> a/2
    ((< a 5) (- a 1))   ;; elif (a < 5)         ==> a-1
    (t 7)               ;; else                 ==> 7
    )
2
ELISP>

2.14.3 CL-Case - Case Switch

(defun test-cl-case (operation x y)
  (cl-case operation
    (:mul (* x y))
    (:add (+ x y))
    (:sub (- x y))
    (:div (/ x y))
    (otherwise nil)))

ELISP> (test-cl-case :mul 2 10)
20

ELISP> (test-cl-case :sub 10 2)
8

ELISP> (test-cl-case :add 10 2)
12
ELISP> (test-cl-case :div 10 2)
5

ELISP> (test-cl-case 'dummy 20 10)
nil

2.14.4 循环

Dolist

ELISP> (dolist (h '(a b c)) (print h))

a

b

c

nil

ELISP> (dolist (x '(1 2 3)) (print (* 2 x)))

2

4

6

nil
ELISP>

ELISP> (dolist (x '(1 2 3))
     (dolist (y '(a b))
        (print (list x y))))
(1 a)

(1 b)

(2 a)

(2 b)

(3 a)

(3 b)

nil
ELISP>

Dotimes

ELISP> (dotimes (i 3) (print i))

0

1

2

nil
ELISP

ELISP> (dotimes (i 3) (print (* 2 i)))

0

2

4

nil
ELISP>

Loop

最好使用 mapfilter 代替 loops , 详见 Functional Programming

ELISP> (setq a 4)
4

ELISP> (loop
    (setq a (+ a 1))
    (when (> a 7) (return a)))
8

ELISP> a
8
ELISP>

ELISP> (loop
   (setq a (- a 1))
   (when (< a 3) (return)))
nil
ELISP> a
2
ELISP>

Loop Collecting / Summing / For

ELISP> (loop for i from 1 to 10 collecting i)
(1 2 3 4 5 6 7 8 9 10)

ELISP> (loop for i from 1 to 10 collecting (* 3 i))
(3 6 9 12 15 18 21 24 27 30)

ELISP> (loop for x from 1 to 10 summing (expt x 2))
385

ELISP> (loop for x from 1 to 10 collecting (* 2 x))
(2 4 6 8 10 12 14 16 18 20)

ELISP> (loop for x from 1 to 10 summing (* 2 x))
110
ELISP>

ELISP> (apply #'+ '(2 4 6 8 10 12 14 16 18 20))
110

ELISP> (loop for i below 10 collecting i)
(0 1 2 3 4 5 6 7 8 9)

ELISP>  (loop for x in '(1 2 3)
      do (print x) )

1

2

3

nil

(loop
       for x in '(a b c)
       for y in '(1 2 3 4 5 6)
       collect (list x y))
((a 1)
 (b 2)
 (c 3))

ELISP> (loop for (a b) in '((x 1) (y 2) (z 3))
      collect (list b a))
((1 x)
 (2 y)
 (3 z))

ELISP> (loop for i upto 20
      if (oddp i)
    collect i into odds
      else
    collect i into evens
      finally (return (values evens odds)))
((0 2 4 6 8 10 12 14 16 18 20)
 (1 3 5 7 9 11 13 15 17 19))

Do Loop

(do (variable-definition*)
    (end-test-form result-form*)
  statement*)
(do
   ;; Variables Definitions
   ((i 0 (1+ i)))

   ;; Test form
    ((>= i 4))

  ;; Statement form
  (print i))

0

1

2

3
nil

;; Fibbonaci Computing Loop
;;
(do ((n 0 (1+ n))
     (cur 0 next)
     (next 1 (+ cur next)))
    ((= 10 n) cur))
55

2.14.5 函数式编程

Dash 是emacs经常使用的函数式编程库。

  • Map and Filter

Mapcar / Equivalent to map

ELISP> (defun my-fun (x) (* x 10))
my-fun
ELISP>

ELISP> (mapcar 'my-fun '(1 2 3 5 6))
(10 20 30 50 60)

ELISP> (mapcar 'capitalize '("hello" "world" "emacs"))
("Hello" "World" "Emacs")

;;  Anonymous Functions
;;
ELISP> (mapcar (lambda (x) (* x x))   '(1 2 3 4 5 6))
(1 4 9 16 25 36)


ELISP> (setq anon (lambda (x) (* x x)))
(lambda
  (x)
  (* x x))

ELISP> (mapcar anon '(1 2 3 4 5 6))
(1 4 9 16 25 36)

Filter

ELISP> (null nil)
t
ELISP> (null 23)
nil
ELISP>

;; Equivalent to  Haskell idiom:
;;
;; > filter predicate list
;;
ELISP> (remove-if-not 'null '(1 2 3 nil 5 6 nil nil ))
(nil nil nil)

;; Equivalent to Haskell idiom:
;;
;;   > filter (\x -> not (predicate x)) list
;;
;; a more apropriate name would be reject
;;
ELISP> (remove-if 'null '(1 2 3 nil 5 6 nil nil ))
(1 2 3 5 6)



ELISP> (defun range (step start stop)
  (if (> start stop)
      nil
      (cons start (range step (+ step start) stop))

  );; End If
);; End range

ELISP> (range 1 0 10)
(0 1 2 3 4 5 6 7 8 9 10)

ELISP> (range 2 0 20)
(0 2 4 6 8 10 12 14 16 18 20)


ELISP> (remove-if (lambda (x) (= (% x 2) 0)) (range 1 0 20))
(1 3 5 7 9 11 13 15 17 19)

ELISP> (remove-if-not (lambda (x) (= (% x 2) 0)) (range 1 0 20))
(0 2 4 6 8 10 12 14 16 18 20)


ELISP> (remove-if (lambda (x) (= (% x 3) 0)) (range 1 0 20))
(1 2 4 5 7 8 10 11 13 14 16 17 19 20)

ELISP> (remove-if-not (lambda (x) (= (% x 3) 0)) (range 1 0 20))
(0 3 6 9 12 15 18)

ELISP>
  • 匿名函数/lambda函数
ELISP> (lambda (x)(* x 10))
(lambda
  (x)
  (* x 10))

ELISP>

ELISP> (funcall (lambda (x)(* x 10)) 5)
50
ELISP>

ELISP> (setq my-lambda (lambda (x) (+ (* x 10) 5))) ;; 10 * x + 5
(lambda
  (x)
  (+
   (* x 10)
   5))

ELISP> (funcall my-lambda 10)
105
ELISP> (mapcar my-lambda '(1 2 3 4 5))
(15 25 35 45 55)


ELISP>  (setq double (function (lambda (x) (+ x x)) ))
(lambda
  (x)
  (+ x x))

ELISP> (funcall double 22)
44
ELISP>


;;
;; Apply a function to a list of arguments
;;
;;;;;;;;;;;

ELISP> (apply #'+ '(1 2 3 4 5))
15
ELISP>

ELISP>
ELISP> (defun f (x y z) (+ (* 10 x) (* -4 y) (* 5 z)))
f
ELISP> (f 2 3 5)
33

ELISP> (apply 'f '(2 3 5))
33


ELISP> (mapcar (lambda (x) (apply 'f x)) '( (2 3 5) (4 5 6) (8 9 5)))
(33 50 69)



;; Create Higher Order Functions
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  • Function Composition ????
ELISP> ;; ID: f0c736a9-afec-3e3f-455c-40997023e130
(defun compose (&rest funs)
  "Return function composed of FUNS."
  (lexical-let ((lex-funs funs))
    (lambda (&rest args)
      (reduce 'funcall (butlast lex-funs)
          :from-end t
          :initial-value (apply (car (last lex-funs)) args)))))
          compose

ELISP> (funcall (compose 'prin1-to-string 'random* 'exp) 10)
"4757.245739507558"
ELISP>
  • Interactive Functions
(defun some-interactive-function ()
   "Documentation"
  (interactive)
  ...)
  • List Recursive Functions

Map

(defun map (fun xs)
  (if (null xs)
      '()
    (cons (funcall fun (car xs))
      (map fun (cdr xs)))))

ELISP> (map #'buffer-name (buffer-list))
("*ielm*" "*scratch*" " *Minibuf-1*" "*Backtrace*" "*eshell*" "sclj.import.scm" "*Messages*" "*GNU Emacs*" " *Minibuf-0*" " *code-conversion-work*" " *Echo Area 0*" " *Echo Area 1*" "*Shell Command Output*" "*Completions*")

ELISP>

Filter

(defun filter (fun xs)
  (if (null xs)
      '()
    (let ((hd (car xs))
      (tl (cdr xs)))
      (if (funcall fun hd)
      (cons hd (filter fun tl))
    (filter fun tl)))))

(defun odd? (x) (zerop (% x 2)))

ELISP> (filter #'odd? '(1 2 3 4 5 6))
(2 4 6)

Take

(defun take (n xs)
  (if (or (null xs) (zerop n))
      '()
    (cons (car xs)
          (take (- n 1) (cdr xs)))))


ELISP> (take 5 '(a b c d e f g h i j))
(a b c d e)

ELISP> (take 10 '(a b c d e f g h i j))
(a b c d e f g h i j)

ELISP> (take 200 '(a b c d e f g h i j))
(a b c d e f g h i j)

ELISP> (take 0 '(a b c d e f g h i j))
nil
ELISP> (take 10 '())
nil
ELISP>

Drop

(defun drop (n xs)
  (if (or (null xs) (zerop n))
      xs
    (drop (- n 1)  (cdr xs))))

ELISP> (drop 3 '(a b c d e f g h i j))
(d e f g h i j)

ELISP> (drop 4 '(a b c d e f g h i j))
(e f g h i j)

ELISP> (drop 25 '(a b c d e f g h i j))
nil
ELISP>

Map-apply

(defun map-apply (fun xss)
  (mapcar (lambda (xs) (apply fun xs)) xss))

ELISP> (map-apply #'fxyz '((1 2 3) (3 4 5) (2 3 1)))
(17 35 20)

ELISP> (fxyz 1 2 3)
17
ELISP> (fxyz 3 4 5)
35
ELISP> (fxyz 2 3 1)
20
ELISP>

Zip

(defun zip (&rest xss)
  (if (null (car xss))
      '()
    (cons
     (mapcar #'car xss)
     (apply #'zip (mapcar #'cdr xss)))))

ELISP> (zip (list 1 2 3 4) '(a b c d) '(x y z w))
((1 a x)
 (2 b y)
 (3 c z)
 (4 d w))

Zipwith

(defun zipwith (f &rest xss)
  (map-apply f (apply #'zip xss)))

ELISP> (zipwith #'f '(1 2 3) '(4 5 6) '(3 6 8))
(23 40 53)

ELISP> (f 1 4 3)
23

ELISP> (f 2 5 6)
40

ELISP> (f 3 6 8)
53
ELISP>

Foldr

;;           f :: x -> acc -> acc
;; foldr :: (a -> b -> b) -> b -> [a] -> b
;; foldr :: (x -> acc -> acc) -> acc -> [x] -> acc
;; foldr f z []     = z
;; foldr f z (x:xs) = f x (foldr f z xs)
;;
;;  x = (car xss) , xs = (cdr xss)
(defun foldr (f acc xss)
  (if (null xss)
      ;; foldr f z []     = z
      acc
    ;; foldr f z (x:xs) = f x (foldr f z xs)
    (funcall f (car xss)
             (foldr f acc (cdr xss)))))

ELISP> (foldr (lambda (a b) (+ (* 10 b) a)) 0 '(1 2 3 4 5))
54321
ELISP>

ELISP> (foldr #'+ 0 '(1 2 3 4 5))
15
ELISP>

Foldl

;; foldl :: (b -> a -> b) -> b -> [a] -> b
;; foldl f z []     = z
;; foldl f z (x:xs) = foldl f (f z x) xs
(defun foldl (f acc xss)
  (if (null xss)
      acc
    (foldl f (funcall f acc (car xss)) (cdr xss))))

ELISP> (foldl (lambda (a b) (+ (* 10 a) b)) 0 '(1 2 3 4 5))
12345
ELISP>

Map Pairs

(defun map-pair (func xs)
  (mapcar (lambda (x) (cons x (funcall func x))) xs))

ELISP> (map-pair #'1+ '(1 2 3 4))
((1 . 2)
 (2 . 3)
 (3 . 4)
 (4 . 5))

ELISP> (map-pair #'log10 '(1 10 100 1000 10000))
((1 . 0.0)
 (10 . 1.0)
 (100 . 2.0)
 (1000 . 3.0)
 (10000 . 4.0))

(defun buffer-mode (buffer-or-string)
  "Returns the major mode associated with a buffer."
  (with-current-buffer buffer-or-string
    major-mode))

ELISP> (map-pair #'buffer-mode (buffer-list))
((#<buffer *ielm*> . inferior-emacs-lisp-mode)
 (#<buffer *scratch*> . lisp-interaction-mode)
 (#<buffer *Backtrace*> . debugger-mode)
 (#<buffer *GNU Emacs*> . fundamental-mode)
 (#<buffer  *Minibuf-1*> . minibuffer-inactive-mode)
 (#<buffer  *Minibuf-0*> . minibuffer-inactive-mode)
 (#<buffer *Messages*> . messages-buffer-mode)

Map pairs xy

(defun map-xypair (func-x func-y xs)
  (mapcar
   (lambda (x)
     (cons (funcall func-x x) (funcall func-y x)))
   xs))

ELISP> (map-xypair #'buffer-name #'buffer-mode (buffer-list))
(("*ielm*" . inferior-emacs-lisp-mode)
 ("*scratch*" . lisp-interaction-mode)
 ("*Backtrace*" . debugger-mode)
 ("*GNU Emacs*" . fundamental-mode)
 (" *Minibuf-1*" . minibuffer-inactive-mode)
 (" *Minibuf-0*" . minibuffer-inactive-mode)
 ("*Messages*" . messages-buffer-mode)
 (" *code-conversion-work*" . fundamental-mode)
 (" *Echo Area 0*" . fundamental-mode)
 (" *Echo Area 1*" . fundamental-mode)
 (" *http www.httpbin.org:80*" . fundamental-mode)
 (" *http www.httpbin.org:80*-820734" . fundamental-mode)
 (" *http www.httpbin.org:80*-914099" . fundamental-mode)
 (" *http www.httpbin.org:80*-945998" . fundamental-mode)
 ("*Help*" . help-mode)
 ("*Completions*" . completion-list-mode))

Juxt

ELISP> (juxt #'buffer-name #'buffer-mode)
(lambda
  (x)
  (list
   ((funcall #'buffer-name x)
    (funcall #'buffer-mode x))))


ELISP> (funcall (juxt #'buffer-file-name  #'buffer-name #'buffer-mode) (current-buffer))
(nil "*ielm*" inferior-emacs-lisp-mode)

ELISP> (mapcar (juxt #'buffer-name #'buffer-file-name #'buffer-mode) (buffer-list))
(("*ielm*" nil inferior-emacs-lisp-mode)
 ("*scratch*" nil lisp-interaction-mode)
 ("passgen.py" "/home/tux/bin/passgen.py" python-mode)
 (".bashrc" "/home/tux/.bashrc" sh-mode)
 (" *Minibuf-1*" nil minibuffer-inactive-mode)
 ("init.el" "/home/tux/.emacs.d/init.el" emacs-lisp-mode)
 ("*Backtrace*" nil debugger-mode)
 ("*GNU Emacs*" nil fundamental-mode)
 (" *Minibuf-0*" nil minibuffer-inactive-mode)
 ("*Messages*" nil messages-buffer-mode)
 (" *code-conversion-work*" nil fundamental-mode)
 (" *Echo Area 0*" nil fundamental-mode)
 (" *Echo Area 1*" nil fundamental-mode)
 (" *http www.httpbin.org:80*" nil fundamental-mode)
 (" *http www.httpbin.org:80*-820734" nil fundamental-mode)
 (" *http www.httpbin.org:80*-914099" nil fundamental-mode)
 (" *http www.httpbin.org:80*-945998" nil fundamental-mode)
 ("*Help*" nil help-mode)
 ("*Completions*" nil completion-list-mode))

Map Juxt

(defmacro map-juxt (xs_f xs)
  `(mapcar (juxt ,@xs_f) ,xs))


ELISP> (map-juxt (#'buffer-name #'buffer-file-name #'buffer-mode) (buffer-list))
(("*ielm*" nil inferior-emacs-lisp-mode)
 ("*scratch*" nil lisp-interaction-mode)
 ("passgen.py" "/home/tux/bin/passgen.py" python-mode)
 (".bashrc" "/home/tux/.bashrc" sh-mode)
 (" *Minibuf-1*" nil minibuffer-inactive-mode)
 ("init.el" "/home/tux/.emacs.d/init.el" emacs-lisp-mode)
 ("*Backtrace*" nil debugger-mode)
 ("*GNU Emacs*" nil fundamental-mode)
 (" *Minibuf-0*" nil minibuffer-inactive-mode)
 ("*Messages*" nil messages-buffer-mode)
 ...

Lambda Function Macro

(defmacro $f (f &rest params)
  `(lambda ($) (,f ,@params)))


ELISP> ($f - 10 $)
(lambda
  ($)
  (- 10 $))

ELISP> ($f * (+ 3 $) 5)
(lambda
  ($)
  (*
   (+ 3 $)
   5))

ELISP> (funcall ($f * (+ 3 $) 5) 10)
65
ELISP> (mapcar  ($f * (+ 3 $) 5) '(1 2 3 4 5))
(20 25 30 35 40)

ELISP>
ELISP> (mapcar  ($f list (1+ $) (1- $) (log10 $)) '(1 10 100 1000))
((2 0 0.0)
 (11 9 1.0)
 (101 99 2.0)
 (1001 999 3.0))

Partial Application

(defmacro $c (f  &rest params)
  `(lambda (__x) (,f ,@params __x)))

ELISP> (defun f (x y z) (+ (* 3 x) (* 2 y) (* 4 z)))
f
ELISP> (f 1 2 3)
19
ELISP> ($c f 1 2)
(lambda
  (__x)
  (f 1 2 __x))

ELISP> (mapcar ($c f 1 2) '(1 2 3 4 5))
(11 15 19 23 27)

ELISP> (mapcar ($c + 1 2) '(1 2 3 4 5))
(4 5 6 7 8)

ELISP>

2.15 Structures

ELISP> (defstruct account id name balance)
account
ELISP> (make-account :id 3434 :name "John" :balance 1000.34)
[cl-struct-account 3434 "John" 1000.34]

ELISP> (setq user1 (make-account :id 3434 :name "John" :balance 1000.34))
[cl-struct-account 3434 "John" 1000.34]

ELISP> (account-name user1)
"John"

ELISP> (account-id user1)
3434

ELISP> (account-balance user1)
1000.34

;; Test if input is an account object
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ELISP> (account-p user1)
t
ELISP>

;; Change Field
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (defun withdraw (accc amount)
         (setf (account-balance acc) (- (account-balance acc) amount)))
withdraw

ELISP> (withdraw user1 300)
700.34
ELISP> user1
[cl-struct-account 3434 "John" 700.34]

ELISP> (withdraw user1 500)
200.34000000000003
ELISP> user1
[cl-struct-account 3434 "John" 200.34000000000003]

ELISP>

;; Build structure from a list of parameters
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (defun build-account (id name balance)
         (make-account :id id :name name  :balance balance))
build-account

ELISP> (build-account 3434 "O' Neil" 35434.23)
[cl-struct-account 3434 "O' Neil" 35434.23]

ELISP> (apply 'build-account '(3434 "O' Neil" 35434.23))
[cl-struct-account 3434 "O' Neil" 35434.23]

ELISP>

ELISP> (mapcar (lambda (params) (apply 'build-account params))
               '(
                 (34423 "O' Neil" 23.2323)
                 (1023  "John Edwards" 1002323.23)
                 (92323 "Mr. Dummy"  2323241.2323)
                 (8723  "John Oliver" 9823)
                 ))
([cl-struct-account 34423 "O' Neil" 23.2323]
 [cl-struct-account 1023 "John Edwards" 1002323.23]
 [cl-struct-account 92323 "Mr. Dummy" 2323241.2323]
 [cl-struct-account 8723 "John Oliver" 9823])

ELISP>

ELISP> (defun build-accounts-from-list (list-of-params)
         (mapcar (lambda (params) (apply 'build-account params)) list-of-params))
build-accounts-from-list
ELISP>

ELISP> (setq accounts (build-accounts-from-list
                       '(
                         (34423 "O' Neil" 23.2323)
                         (1023  "John Edwards" 1002323.23)
                         (92323 "Mr. Dummy"  2323241.2323)
                         (8723  "John Oliver" 9823)
                         )))
([cl-struct-account 34423 "O' Neil" 23.2323]
 [cl-struct-account 1023 "John Edwards" 1002323.23]
 [cl-struct-account 92323 "Mr. Dummy" 2323241.2323]
 [cl-struct-account 8723 "John Oliver" 9823])

ELISP> accounts
([cl-struct-account 34423 "O' Neil" 23.2323]
 [cl-struct-account 1023 "John Edwards" 1002323.23]
 [cl-struct-account 92323 "Mr. Dummy" 2323241.2323]
 [cl-struct-account 8723 "John Oliver" 9823])

ELISP> (mapcar #'account-id accounts)
(34423 1023 92323 8723)

ELISP>

ELISP>
ELISP> (mapcar #'account-name accounts)
("O' Neil" "John Edwards" "Mr. Dummy" "John Oliver")

ELISP>


ELISP> (mapcar #'account-balance accounts)
(23.2323 1002323.23 2323241.2323 9823)

ELISP>

3 宏和元编程

3.1 Quasi-quote

;;;; Quasiquote

> `(the product of 3 and 4 is ,(* 3 4))
(the product of 3 and 4 is 12)

> `("the product of 3 and 4 is" ,(* 3 4))
("the product of 3 and 4 is" 12)

> `("the value of (exp 3) is " ,(exp 3) "the value of (sqrt 100) is" ,(sqrt 100))
("the value of (exp 3) is " 20.085536923187668 "the value of (sqrt 100) is" 10.0)

> `(a ,a b ,b c ,c d ,d)
(a 10 b 20 c my-symbol d "a string")

> `((a . ,a) (b . ,b) (c . ,c) (d . ,d))
((a . 10)
 (b . 20)
 (c . my-symbol)
 (d . "a string"))

> (setq xs '(sym1 sym2 sym3))
(sym1 sym2 sym3)

> xs
(sym1 sym2 sym3)

> `(xs ,xs)
(xs
 (sym1 sym2 sym3))

> `(xs ,@xs)
(xs sym1 sym2 sym3)

> `(if (< ,a ,b) ,(+ a 4) ,d)
(if
    (< 10 20)
    14 "a string")

> (eval `(if (< ,a ,b) ,(+ a 4) ,d))
14
>

> (eval `(if (> ,a ,b) ,(+ a 4) ,d))
"a string"

;;------------------

> (setq xlist '(1 2 3 4))
(1 2 3 4)

> (setq ylist '(a b c d e))
(a b c d e)

> `(xs ,xlist ys ,ylist)
(xs
 (1 2 3 4)
 ys
 (a b c d e))

> `(xs ,@xlist ys ,@ylist)
(xs 1 2 3 4 ys a b c d e)

3.2

定义lambda函数语法糖:λ

(defmacro λ (args body)
  `(lambda ,args ,body))

ELISP> (λ (x) (+ x 3))
(lambda
  (x)
  (+ x 3))
ELISP> (mapcar (λ (x) (+ x 3)) '(1 2 3 4 5 6))
(4 5 6 7 8 9)

Set variable to nil

(defmacro nil! (var)
  `(setq ,var nil))

ELISP> (setq x 10)
10
ELISP> x
10
ELISP>

ELISP> (nil! x)
nil
ELISP> x
nil
ELISP>

ELISP> (nil! z)
nil
ELISP> z
nil
ELISP>

Create Clojure def, defn and fn special forms

(defmacro fn (args body)
  `(lambda ,args ,body))

(defmacro def (name value)
  `(setq ,name ,value))

(defmacro defn (name args body)
  `(defun ,name ,args ,body))

ELISP> (fn (x) (* x x))
(lambda
  (x)
  (* x x))

ELISP> (mapcar (fn (x) (* x x)) '(1 2 3 4 5))
(1 4 9 16 25)

ELISP> (def x 1000)
1000
ELISP> x
1000
ELISP>

ELISP> (defn f (x y z) (+ (* 3 x) (* -4 y) (* 5 z)))
f
ELISP> (f 4 5 6)
22
ELISP>

……

4 Emacs API

4.1 Emacs术语

Emacs Terminology Description
Point Cursor position, number of characters from beggining of the buffer to current cursor position.
Buffer Place where the user edit something. Not all buffers are bound to a file.
Mark Beginning of the selected area.
Region Selected area/ text
Frame The current window of emacs
Windows Each frame can be split in sections that Emacs documentation calls windows
Fill Word Wrap
Yank Copy
Kill Region Cut
Kill Ring Clipboard
Kill Buffer Close Buffer
Mode Line Status Bar
Font Locking Syntax Coloring

Ben's Journal: 11 Concepts The Emacs Newbie Should Master

4.2 Emacs API

API对象

  • Buffer
  • Temporary Buffer
  • Modes
  • Mode Hooks
  • Mode Map
  • Window
  • Frame
  • Point
  • Process
  • Network Process
  • Minibuffers

4.3 Buffers

4.3.1 Buffer Attributes

(buffer-list)
(current-buffer)
(mapcar #'buffer-name (buffer-list))
(mapcar #'buffer-file-name (buffer-list))
(kill-buffer "init.el")
(get-buffer "*scratch*")

列出打开文件

(defun opened-files ()
  "list all opened file in current session"
  (interactive)
  (remove-if 'null (mapcar 'buffer-file-name (buffer-list))))

(opened-files)

创建新buffer

;;
;;
;; This function returns a buffer named  buffer-or-name.
;; The buffer returned does not become the current
;; buffer—this function does not change which buffer is current.
;;

ELISP> (get-buffer-create "foobar")
#<buffer foobar>
ELISP>

;;
;;  Divide the screen in two windows, and switch to the new buffer
;;  window
;;
ELISP> (switch-to-buffer-other-window "foobar")
#<buffer foobar>
ELISP>

;; Clean Current Buffer
;;
ELISP> (erase-buffer)
nil
ELISP>

;;  Edit another buffer and go back to the old buffer
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (defun within-buffer (name function)
         (let (curbuff (current-buffer))
           (switch-to-buffer name)
           (funcall function)
           (switch-to-buffer current-buffer)
           ))

ELISP> (within-buffer "foobar" (lambda () (insert "dummy")))
#<buffer *ielm*>
ELISP>
ELISP> (lambda (x)(* x 10))
(lambda
  (x)
  (* x 10))

;;;; Translated from: http://d.hatena.ne.jp/rubikitch/20100201/elispsyntax
;;
ELISP> ;; test-buffer Create a buffer named, to write a variety of content
(with-current-buffer (get-buffer-create "test-buffer")
  ;; Empty the contents of the buffer
  (erase-buffer)
  ;; /tmp/foo.txt Make the contents inserted
  (insert-file-contents "/etc/fstab")
  ;; Insert a string
  (insert "End\n")
  ;; Write the contents of a buffer to a file
  (write-region (point-min) (point-max) "/tmp/bar.txt"))
nil
ELISP>

4.3.2 Buffer Mode

Show Buffers Mode

ELISP> (defun buffer-mode (buffer-or-string)
         "Returns the major mode associated with a buffer."
         (with-current-buffer buffer-or-string
           major-mode))
buffer-mode

ELISP> (mapcar (lambda (b)(
                           let
                           (
                            (name (buffer-name b))
                            (type   (buffer-mode (buffer-name b)))
                            )
                           (list name type)
                           ))
               (buffer-list))
(("*ielm*" inferior-emacs-lisp-mode)
 ("*SPEEDBAR*" speedbar-mode)
 (" *Minibuf-1*" minibuffer-inactive-mode)
 ("*scratch*" emacs-lisp-mode)
 ("test3.ml" tuareg-mode)
 ("*Help*" help-mode)
 ("*Messages*" messages-buffer-mode)
 ("sbet.ml" tuareg-mode)
 (" *Minibuf-0*" minibuffer-inactive-mode)
 ("test.el" emacs-lisp-mode)
 ...

4.3.3 Get Buffer Contents / Selection / Line

Get Buffer Content as String

ELISP> (defun buffer-content (name)
         (with-current-buffer name
           (buffer-substring-no-properties (point-min) (point-max))))
buffer-content
ELISP>

ELISP> (buffer-content "test3.ml")
"\n\nlet rec prodlist = function \n    | [] ... "

Get Selected text in current buffer as string

(defun get-selection ()
  "Get the text selected in current buffer as string"
  (interactive)
  (buffer-substring-no-properties (region-beginning) (region-end))
  )

Get current line in current buffer

(defun get-current-line ()
  (interactive)
  "Get current line, where the cursor lies in the current buffer"
  (replace-regexp-in-string "[\n|\s\t]+$" "" (thing-at-point 'line t))
  )

4.3.4 Search and Replace in the entire Buffer

(defun replace-regexp-entire-buffer (pattern replacement)
  "Perform regular-expression replacement throughout buffer."
  (interactive
   (let ((args (query-replace-read-args "Replace" t)))
     (setcdr (cdr args) nil)    ; remove third value returned from query---args
     args))
  (save-excursion
    (goto-char (point-min))
    (while (re-search-forward pattern nil t)
      (replace-match replacement))))

4.4 Point, Region, Line and Buffer

4.4.1 Point

Point

Function Description
(point) Current cursor position
(point-min) Minimum cursor position in current buffer. (always returns 1)
(point-max) Maximum cursor position in current buffer.
   
(line-beginning-position) Point of the beginning of current line.
(line-end-position) Point of the end of current line.
   
(region-beginning) Position of the beginning current region (selected text).
(region-end) Position of the end current region.
   
(bounds-of-thing-at-point <thing>) Returns the cons pair '(beginning . end) position of thing at point.

Point Interface Functions

Function Description
(goto-char <point>) Move the cursor to a given point.
(insert <string>) Insert text at current point.
(buffer-substring [pmin] [pmax]) Returns the text with properties between the points <pmin> and <pmax>.
(buffer-substring-no-properties [pmin] pmax]) Returns the text without properties between the points.
(delete-region [pmin] [pmax]) Deletes the text between <pmin> and <pmax>.
> (point)
99696

> (point-min)
1


> (point-max)
185623

>  (line-beginning-position)
99774

>  (line-end-position)
99804

> (buffer-substring-no-properties
   (line-beginning-position)
   (line-end-position))

(defun delete-line ()
  (interactive)
  (delete-region  (line-beginning-position)  (line-end-position)))

(defun delete-region ()
  (interactive)
  (delete-region  (region-beginning) (region-end)))

(defun insert-end-of-buffer ()
  (interactive)

  ;; Save Current Cursor Position
  ;; and go back to initial positon when
  ;; finish this block
  (save-excursion
    (goto-char (point-max)) ;;; Go to end of buffer
    (insert "Testing insert end of buffer")
    ))

4.4.2 Thing at Point API

???

4.5 Message / Output

(message "Hello world")
(message-box "Time for a break.\nDrink some coffee")

4.6 Files, Directories and Path

4.6.1 Basic Functions

;; Get and Set current directory

ELISP> (pwd)
"Directory /home/tux/tmp/"

ELISP> (cd "/etc/")
"/etc/"

ELISP> (pwd)
"Directory /etc/"
ELISP>


ELISP> (file-name-directory "/etc/hosts")
"/etc/"

;; Expand File Name
;;
ELISP> (expand-file-name "~/")
"/home/tux/"
ELISP> (expand-file-name ".")
"/home/tux/tmp"
ELISP> (expand-file-name "..")
"/home/tux"
ELISP>


;;;;; Create a Directory
;;;
ELISP> (mkdir "dummy")
nil
ELISP> (mkdir "dummy")
** Eval error **  File exists: /home/tux/dummy
ELISP>

;;; List Directory
;;;;
;;;
ELISP> (directory-files "/home/tux/PycharmProjects/Haskell/")
("." ".." ".git" ".gitignore" ".idea" "LICENSE" "Make" "Makefile"
 "README.back.md" "README.html" "README.md" "Test.html" "build.sh" "clean.sh"
 "codes" "dict.sh" "haskell" "ocaml" "papers" "tags" "tmp")

4.6.2 File Name Components

ELISP> (file-name-directory "/usr/bin/env")
"/usr/bin/"
ELISP>

ELISP> (file-name-nondirectory "/usr/bin/env")
"env"
ELISP>


ELISP> (file-name-base "/home/foo/zoo1.c")
"zoo1"
ELISP> (file-name-base "/home/foo/zoo1.c.back")
"zoo1.c"

4.6.3 Read / Write file to a string

Read File

ELISP> (defun file-contents (filename)
  (interactive "fFind file: ")
  (with-temp-buffer
    (insert-file-contents filename) ;; 先将文件内容插入临时buffer,再读取内容
    (buffer-substring-no-properties (point-min) (point-max))))

ELISP> (file-contents "/proc/filesystems")
"nodev  sysfs\nnodev    rootfs\nnodev   ramfs\nnodev
bdev\nnodev proc\nnodev cgroup\nnode ...

Write to File

ELISP> (append-to-file "hello world" nil "/tmp/hello.txt")
nil

ELISP> (file-contents "/tmp/hello.txt")
"hello world"
ELISP>

4.7 Window Functions

4.7.1 Basic Window Functions

(split-window-horizontally)
(split-window-vertically)
(delete-other-windows)
(switch-to-buffer-other-window "init.el")
(delete-window)
(make-frame)
(frame-list)
(delete-frame)

4.7.3 Window Configuration

(current-window-configuration)
(setq w (current-window-configuration))
w
(set-window-configuration w)
;; Screen Resolution

ELISP> (x-display-pixel-width)
1366

ELISP> (x-display-pixel-height)
768
ELISP>
ELISP>

;; Resize and Set Emacs Windows position
;;
;; From: http://uce.uniovi.es/tips/Emacs/mydotemacs.html#sec-41
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

ELISP> (defun resize-frame ()
  "Set size"
  (interactive)
  (set-frame-width (selected-frame) 100)
  (set-frame-height (selected-frame) 28)
  (set-frame-position (selected-frame) 0 1))
resize-frame
ELISP>

ELISP> (resize-frame)
t
ELISP>

4.8 OS Interface

4.8.1 Find the current operating system

Value Description
gnu GNU Hurd system.
gnu/linux GNU/Linux system.
gnu/kfreebsd GNU system with a FreeBSD kernel.
darwin Darwin (GNU-Darwin, Mac OS X)
ms-dos MS-DOS application.
windows-nt native W32 application.
cygwin compiled using the Cygwin library
system-type
system-configuration

4.8.2 Date and Time

(current-time)
(insert (format-time-string "%Y-%m-%d")) ;; 2019-12-15
(insert (format-time-string "%H:%M:%S")) ;; 16:11:04
(format-time-string "%d/%m/%Y %H:%M:%S" (current-time))

4.8.3 Call External Commands or Apps

(call-process "mpd")
(shell-command-to-string "pwd")

4.8.4 Environment Variables

(getenv "PATH")
(split-string (getenv "PATH") ":")
(dolist (e (split-string  (getenv "PATH") ":")) (princ (format "%s\n" e)))
exec-path
(getenv "HOME")
(setenv "JAVA_HOME" "/usr/local/java")

system-type
(eq system-type 'gnu/linux)()
(dolist (e process-environment) (princ (format "%s\n" e)))

4.8.5 Process Management

(process-list)
(get-process "merlin")
(mapcar 'process-name (process-list))

;;;; Buffer Process
(process-command (get-process "vterm"))
(process-id (get-process "vterm"))
(process-buffer (get-process "vterm"))
(buffer-name (process-buffer (get-process "vterm")))
(mapcar (lambda (p) (buffer-name (process-buffer p))) (process-list))
(display-buffer (process-buffer (get-process "vterm")))

;;;; Start Asyncronous Process
;;  Start the process named py, with the buffer named pybff
;;  using the command python, /usr/bin/python (on linux)
(start-process "py" "pybff" "python")
;; End the process named py
(process-send-eof "py")

(process-send-string "py" "print 'Hello world'\n")

;;;; Get Multiple Fields

(mapcar
 (lambda (p)(list
             p
             (process-name p)
             (process-command p)
             (list (process-buffer p) (buffer-name (process-buffer p)))
             (process-id p)
             (process-status p)
             ))
 (process-list))

4.9 Interfaces

4.9.1 Creating Quick Access Menu

(require 'easymenu)

(easy-menu-define djcb-menu global-map "Utils"
  '("Utils"
    ("Shells" ;; submenu
     ["Ielm   - Emacs Lisp Shell"       (ielm)]
     ["Eshell - Emacs Buitin Shell"    (eshell)]
     ["Native Shell "                  (shell)]
     ["---------------------" nil]
     ["Edit .bashrc" (find-file  "~/.bashrc")]
     ["Edit .profile" (find-file "~/.profile")]
     ["Edit .Xresources" (find-file "~/.Xresources")]
     ["Edit .xsessin"    (find-file "~/.xsession")]
     ["See all GNU MAN pages" ( info)]
     ["See a specific Man Page" (woman)]

     );; End of shells menu

    ("Emacs /Elisp"  ;; submenu

     ["Ielm   - Emacs Lisp Shell"  (ielm)]
     ["Eval buffer"   (eval-buffer) ]
     ["---------------------" nil]

     ["Edit  init.el" (find-file  user-init-file)]
     ["Reload init.el" (load-file user-init-file)]
     ["Open .emac.d dir" (find-file "~/.emacs.d")]
     ["List packages"     (list-packages)]
     ["Install package"   (package-install)]

     ) ;; End of Emacs / Elisp submenu
    )) ;; End of Custom Menu

4.10 Timer

4.10.1 run-with-timer

;;; (run-with-timer SECS REPEAT FUNCTION &rest ARGS)
(run-with-timer 5 nil
                (lambda () (message-box "happy hacking emacs!")))

(defun cofee-wait ()
  (interactive)
  (let ((minutes 3))
    (run-with-timer (* 60 minutes)  nil
                    (lambda () (message-box "Coffee done"))
                    )
    (message "Waiting for the cofee")
    ))

4.11 Emacs Modes

4.11.1 Mode Association with Files

;; 列出所有和拓展名相关的mode
auto-mode-alist
;; 列出与一个mode相关的所有拓展名
(remove-if-not
 (lambda (al) (equal (cdr al) 'web-mode)) auto-mode-alist)
;; 为一个mode关联拓展名
(add-to-list 'auto-mode-alist '("\\.markdown\\'" . markdown-mode))

4.11.2 Lisp Routines to introspect modes??

(defun show-doc (function)
  (princ (documentation function)))

(defun mode/show ()
  "  Returns all modes associated with files

     To query the file extesions associated with a mode
     use:
         > (mode/ftypes 'markdown-mode)

     for example.
  "
  (dolist (m (remove-if #'listp
                        (mapcar #'cdr auto-mode-alist))) (print m)))

(defun mode/ftypes (mode)
  "
  Get all file extension associated with a mode.

  Usage:

  ELISP> (get-mode-ftypes 'markdown-mode)
  ((\"\\.md\\'\" . markdown-mode)
  (\"\\.text\\'\" . markdown-mode)
  (\"\\.markdown\\'\" . markdown-mode)

  "
  (remove-if-not
   (lambda (al)
     (equal (cdr al) mode))
   auto-mode-alist))

ELISP> (mode/ftypes 'clojure-mode)
(("\\(?:build\\|profile\\)\\.boot\\'" . clojure-mode)
 ("\\.\\(clj\\|dtm\\|edn\\)\\'" . clojure-mode))

ELISP> (mode/ftypes 'scheme-mode)
(("\\.\\(scm\\|stk\\|ss\\|sch\\)\\'" . scheme-mode)
 ("\\.scm\\.[0-9]*\\'" . scheme-mode)
 ("\\.oak\\'" . scheme-mode))

ELISP> (show-doc #'mode/ftypes)

Get all file extension associated with a mode.

Usage:

ELISP> (get-mode-ftypes 'markdown-mode)
(("\.md\'" . markdown-mode)
 ("\.text\'" . markdown-mode)
 ("\.markdown\'" . markdown-mode))

4.11.3 Mode Specific Key Bindings

(define-key emacs-lisp-mode-map (kbd "<f5>")
  (lambda () (interactive) (message "Hello world")))
(add-hook 'lisp-interaction-mode-hook 'turn-on-eldoc-mode)

4.12 Special Variables

emacs-major-version
load-path
window-system
system-type
system-configuration
shell-file-name
user-full-name
user-mail-address
user-init-file
user-emacs-directory
exec-directory

5 正则表达式

5.1 Emacs Regex

Special characters

. any character (but newline)
* previous character or group, repeated 0 or more time
+ previous character or group, repeated 1 or more time
? previous character or group, repeated 0 or 1 time
^ start of line
$ end of line
[…] any character between brackets
[^..] any character not in the brackets
[a-z] any character between a and z
\ prevents interpretation of following special char
\ or
\w word constituent
\b word boundary
\sc character with c syntax (e.g. \s- for whitespace char)
  start\end of group
\< \> start\end of word
\` \' start\end of buffer
\1 string matched by the first group
\n string matched by the nth group
\{3\} previous character or group, repeated 3 times
\{3,\} previous character or group, repeated 3 or more times
\{3,6\} previous character or group, repeated 3 to 6 times

POSIX Character classes

[:digit:] digit, same as [0-9]
[:upper:] letter in uppercase
[:space:] whitespace character, as defined by the syntax table
[:xdigit:] hexadecimal digit
[:cntrl:] control character
[:ascii:] ascii character

Syntax Classes

\s- whitespace character \s/ character quote character
\sw word constituent \s$ paired delimiter
\s_ symbol constituent \s' expression prefix
\s. punctuation character \s< comment starter
\s( open delimiter character \s> comment starter
\s) close delimiter character \s! generic comment delimiter
\s" string quote character \s generic string delimiter
\s\ escape character    

Emacs X Perl Regex

Emacs Regex Perl Regex Description
  ( ) Capture group
\{ \} { }  
\s- \s White space
\1, \2, \3, \4 $1, $2, $3 Result of capture: search, replace.
[ ] [ ] Character class
[0-9] or [:digit:] \d Digit from 0 to 9
\b \b Word boundary
\w \w Word character

5.2 Regex Commands

C-M-s incremental forward search matching regexp
C-M-r incremental backward search matching regexp

Buffer Commands

M-x replace-regexp replace string matching regexp
M-x query-replace-regexp same, but query before each replacement
M-x align-regexp align, using strings matching regexp as delimiters
M-x highlight-regexp highlight strings matching regexp
M-x grep call unix grep command and put result in a buffer
M-x lgrep user-friendly interface to the grep command
M-x rgrep recursive grep
M-x dired-do-copy-regexp copy files with names matching regexp
M-x dired-do-rename-regexp rename files matching regexp
M-x find-grep-dired display files containing matches for regexp with Dired

Line Commands

Command (M-x command) Alias Description
keep-lines delete-non-matching-lines Delete all lines except those containing matches
flush-lines delete-matching-lines Delete lines containing matches
highlight-lines-matching-regexp hi-lock-line-face-buffer Highlight lines matching regexp
occur list-matching-lines Show lines containing a match
multi-occur   Show lines in all buffers containing a match
how-many count-matches Count the number of strings matching regexp

5.3 Regex Functions

5.3.1 match-string

5.3.2 match-end

5.3.3 match-beginning

5.3.4 re-search

5.3.5 re-search-forward

5.3.6 replace-string-in-regexp

5.3.7 replace-string

5.4 Build regex interactively

M-x re-builder

M-x query-replace-regexp

5.5 Emacs Regex rx-notation

(require 'rx)

;;  (rx <patterns>)

ELISP> (rx digit)
"[[:digit:]]"

ELISP> (rx-to-string '(or "foo" "bar"))
"\\(?:\\(?:bar\\|foo\\)\\)"
Description rx notation Emacs regex
Beginning of Line bol ^
End of Line eol $
     
Begining of String bos \\`
End of String eos \\'
     
Beginning of Word bow \\<
End of Word eow \\>
     
Digit 0 to 9 digit \lbr\lbr:digit:\rbr\rbr
Hexadecimal digit hex \lbr\lbr:xdigit:\rbr\rbr
Match ascii character    
Match anything lower case lower \lbr\lbr:lower:\rbr\rbr
Match anything upper case upper \lbr\lbr:upper:\rbr\rbr
     
word   \sw

example

ELISP> (require 'rx)
rx

ELISP> (rx (+ digit))
"[[:digit:]]+"

ELISP> (rx digit (+ digit))
"[[:digit:]][[:digit:]]+"

ELISP> (rx bol (+ digit) eol)
"^[[:digit:]]+$"

ELISP> (rx (zero-or-more digit))
"[[:digit:]]*"

ELISP> (rx (one-or-more digit))
"[[:digit:]]+"

ELISP> (rx (or "cat" "rat" "dog"))
"\\(?:cat\\|dog\\|rat\\)"

;; (replace-regexp-in-string REGEXP REP STRING
;;      &optional FIXEDCASE LITERAL SUBEXP START)

ELISP> (replace-regexp-in-string
          (rx (or "cat" "rat" "dog"))
          ""
          "cat cata rat rat dograt dog cat2334 23rat2")
" a     2334 232"

;; Replaces only in the beggining of line
;;
ELISP>  (replace-regexp-in-string
          (rx bol (or "cat" "rat" "dog"))
          ""
          "cat cata rat rat dograt dog cat2334 23rat2")
" cata rat rat dograt dog cat2334 23rat2"


ELISP>  (replace-regexp-in-string
          (rx bow (or "cat" "rat" "dog") eow)
          ""
          "cat cata rat rat dograt dog cat2334 23rat2")
" cata   dograt  cat2334 23rat2"

ELISP>  (rx bow (or "cat" "rat" "dog") eow)
"\\<\\(?:cat\\|dog\\|rat\\)\\>"

;;  Removes all whitespaces
;;
ELISP>  (replace-regexp-in-string
          (rx (* whitespace))
          ""
          "cat cata rat rat dograt dog cat2334 23rat2")

"catcataratratdogratdogcat233423rat"

ELISP>  (replace-regexp-in-string
          (rx (* whitespace))
          ""
          "cat cata rat rat dograt dog cat2334 23rat2")

"catcataratratdogratdogcat233423rat"

;; Capture group
;;
ELISP>  (replace-regexp-in-string
          (rx (submatch bow (or "cat" "rat" "dog") eow))
          "(\\1)"
          "cat cata rat rat dograt dog cat2334 23rat2")

"(cat) cata (rat) (rat) dograt (dog) cat2334 23rat2"

ELISP> (rx (submatch bow (or "cat" "rat" "dog") eow))
"\\(\\<\\(?:cat\\|dog\\|rat\\)\\>\\)"
发布于 2019-11-12