company-yasnippet-autoparens 1.0发布 (让company自动提示带括号的过程调用 19/2/15更新)

该插件灵感源自于之前自己提的一个问题:怎么用yasnippent实现优雅的带括号的过程调用?

简单描述就是:

当你输入的语法形式是固定的if cond cons car cdr 等时,可以自动展开snippet,并将光标移动到括号内。但是当输入的语法形式不是以上固定形式,比如:需要做一个过程调用(foo),此时你必须先输入(),然后把光标移动到括号里面(|)再输入方法的名字,这非常不方便。

company-yasnippet-autoparens 就是解决这个问题,它能让company自动提示带括号的过程调用。

例子1:输入任意符号,都会在完成列表里附带一个对应的过程调用的候选项

GIF1

例子2:原本的补完项,现在也会附带一个对应的过程调用的候选项

GIF2

例子3:综合了let snippet 和 过程调用

GIF3

注意:在上面第3个例子里,我为let 增加了var-exp对,但没有破坏snippet状态。这使得我们在写Lisp算法的时候可以一气呵成,不需要思考在什么地方加括号(这一点在以前是做不到的)。

安装方法:

  1. 安装 yasnippet

  2. 如果你已经安装了company,卸载它:

    卸载company最简单的方法是删除company目录(比如:在windows下 C:\Users%YOU-USERNAME%\AppData\Roaming.emacs.d\company-XXXXXXXX.XXXX),并且确保你的Emacs配置不会再次自动安装comapny。

  3. 拷贝company-yasnippet-autoparens.el 到你的任意脚本能加载到的路径(比如:在windows下 C:\Users\你的用户名\AppData\Roaming.emacs.d\)

  4. 在你的emacs配置文件(如:init.el)里加入:

(require 'company)
(global-company-mode 1)

 (defvar company-mode/enable-yas t
   "Enable yasnippet for all backends.")

 ;; Add yasnippet support for all company backends
 (defun company-mode/backend-with-yas (backend)
   (if (or (not company-mode/enable-yas) (and (listp backend) (member 'company-yasnippet backend)))
       backend
     (append (if (consp backend) backend (list backend))
             '(:with company-yasnippet))))

 (setq company-backends (mapcar #'company-mode/backend-with-yas company-backends))

 ;; Add company-yasnippet-autoparens
 (defun company-mode/backend-with-yas-ap (backend)
   (if (or (not company-mode/enable-yas) (and (listp backend) (member 'company-yasnippet-autoparens backend)))
       backend
     (append (if (consp backend) backend (list backend))
             '(:with company-yasnippet-autoparens))))

 (setq company-backends (mapcar #'company-mode/backend-with-yas-ap company-backends))

注:以上使用Elisp只是作为一个例子。事实上company-yasnippet-autoparens 支持任何s-exp语言。因为它并不关注语言本身,而只是给完成列表的每一个候选项增加括号。

TODO:

增加一个Hook机制:


最后说一下 company-yasnippet-autoparens 的不足:

回忆之前第3个例子,我有说过company-yasnippet-autoparens能处理像let那样的变长子表达式(var-exp 对),这一点是没错的,比方说,我现在的let snippet是这样的:

# -*- mode: snippet -*-
# name: (let ([name e]+) e)
# key: let
# --
(let ((${1:var exp})$2)
${3:body})

请注意里面的$2,在写好第1个var-exp对之后,可以TAB到$2的位置,在这个地方你可以按回车。

然后输入第2个var-exp对的var部分,company-yasnippet-autoparens会自动展开成带括号的形式(var),当你输入完var-exp对的exp部分后,可以再按一次TAB,跳出这个var-exp对,到达(var exp) | 这个位置,此时你可以再按一次回车,于是就能输入第3个var-exp对,这个过程非常流畅。

然而,在处理cond表达式的时候会有一点不同(虽然cond和let的变长子表达式的形式很像):

GIF4

这里我为cond 增加了2个条件分支。

请注意我在写完((= x 0) (test-function-1 123)|)后的光标位置,这个时候如果你按TAB的话,会直接跳到otherwise,这样你就无法写第4个分支了。所以此时你需要手动按一下方向键->让当前光标移到((= x 0) (test-function-1 123))|然后再按回车才能输入第4个条件分支。

这个问题的根本原因是 let 和 cond 的语法形式有细微不同:

let的var-exp对里的var必定是一个primitive-exp(一个名字),而cond的pred-conseq对里的pred可以是一个子表达式。因此对于var-exp对来说,其形式上类似于一个单参过程调用(var xxx),但对于pred-conseq对,你必须先手动输入一个括号(pred conseq)(由于这个括号是你手动输入的,因此必须手动跳出,而不能仅仅依靠TAB)

5 个赞

单独拿出来一个repo吧,clone起来不方便

C-h f company

看看文档 有一个post-comp什么的

再者 你没有结合eldoc中的信息

好的,已修改主题。

我不太理解你说的意思?

能不能详细讲讲,你要结合eldoc中的什么信息?

snippet 多了用起来真心蛋疼啊

1 个赞

https://emacs-china.org/search?context=topic&context_id=8497&q=eldoc&skip_context=true

你的意思大概是,函数的元信息(比如:函数有几个参数之类的)也应该显示在列表里。

我认为这个功能普通snippet就能做到的,因为它是静态的(当然我们一般不会用snippet去这种事情,不然的话需要同步snippet和库函数的文档注释是很麻烦的)。

snippet只负责处理和语言相关的语法,并不处理函数的元信息。


company-yasnippet-autoparens 的目的仅仅是对你输入的任意symbol加括号,他不做代码分析(因为lisp大部分都是动态语言,不需要代码分析)

实际上,如果你使用的是静态语言(假如存在一个纯静态类型的lisp语言),company-yasnippet-autoparens 可能就没什么用武之地了。因为静态语言一般都有配套的代码分析器,它可以分析出你当前指针所在位置必须是一个过程调用,而不是一个普通symbol。于是它就可以弹出一个列表显示所有在这里可以调用的过程,这些过程显然都必须是(foo var1 var2)这种形式,而且带文档说明和参数列表等。

C-hv company-completion-finished-hook

你不需要弄一个backend

company-lsp就是在从lsp服务器端获取语法信息后,用snippet展开带签名的函数调用

根本不需要同步snippet还有文档注释,只需要获取一个参数列表的签名就行了,像eldoc一样

ELISP> (substring-no-properties (elisp-get-fnsym-args-string 'car))
"car: (LIST)"

yasnippet完全可以动态展开snippet

(yas-expand-snippet "(car ${1:cons})$0")

同理也不需要做代码分析

你觉得是就是? :zipper_mouth_face:

1 个赞
(defun expand-snippet-from-symfn (symfn)
  (when (or (fboundp symfn)
            (macrop symfn))
    (cl-destructuring-bind
        (name body
              &aux
              (arglist (split-string
                        (string-remove-prefix
                         "("
                         (string-remove-suffix
                          ")" body)) " "))
              (counter 0))
        (thread-first symfn
          (elisp-get-fnsym-args-string)
          (substring-no-properties)
          (split-string ": "))
      (yas-expand-snippet
       (format "(%s %s)$0"
               name
               (apply #'concat
                      (cl-loop
                       for arg in arglist
                       unless (member arg '("&optional"
                                            "&rest"))
                       collect (format "${%d:%s} " (cl-incf counter) arg))))))))

先敲了个初版的,有空改一下给你PR

用dash一时爽,一直用dash一直爽

(defun expand-snippet-from-symfn (symfn)
  (when (or (fboundp symfn)
            (macrop symfn))
    (-let* (((name body) (-> symfn
                             (elisp-get-fnsym-args-string)
                             (substring-no-properties)
                             (split-string ": ")))
            (arglist (->> body
                          (s-chop-prefix "(")
                          (s-chop-suffix ")")
                          (s-split " ")))
            (counter 0))
      (-as-> arglist result
             (--remove (member it '("&optional" "&rest")) result)
             (--map (format "${%d:%s}" (setq counter (1+ counter)) it)
                    result)
             (s-join " " result)
             (yas-expand-snippet (format "(%s %s)$0" name result))))))

我明白你的意思了。

可以抽象一个hook,让外部使用的人去接这个hook:

  1. 如果使用的是elisp就调用elisp-get-fnsym-args-string

  2. 如果是其他方言,可能需要去调用一个语言服务

  3. 如果没有语言服务,可能需要自己做一个简单的过程 扫描函数签名和注释。大部分方言的函数签名是一样的,除了define 和 defun 还有lambda形式,因此理论上可以做成通用库。。。

不过是不是会增加复杂性,我觉得还有必要进一步讨论。

直接打括号的复杂性为 1

1 个赞

你又不用其他lisp方言,杞人憂天做什麼。就算要做,讓解析參數列表後返回一個規範的數據格式不就成了?

Emacs Lisp的macro體和function體都是lambda

所以说,当真不考虑 projectional editor 一定要用手撸?

没有GUI的可怎么办

初学者表示, 用dash确实爽啊, 但是不熟悉 读得晕头转向。。。

整半天就是为了补全一个扩号?

我现在觉得 yasnippet 用来做自动补全就是一个错误!!!

以 c 为例

main<tab>

补全为

int main(int argc, char *argv[])
{
  |光标在此  
  return 0;
}

再来

incs<tab>

补全为

#include <|光标在此>

发现问题所在了吗?

一方面,我们为了少敲几个字破坏了““心流”,我们在尝试记住并使用一种方言(像前面的 incs), 这就有点得不偿失了。 另一方面,有一点违反直觉,一般来说补全都是在我们输入的内容后追加补全的内容,但是 像前面的例子,它在我们输入的 main 前面补了一个 int

我现在是尝试着将 yasnippet 当做一个本子,有意识地去查询并插入,像下面这样子: output-2019-02-14-17%3A30%3A04

关 GUI 啥事,拿 curse 写个 display backend 不就有了

并没有看出你所说的发现问题。

另外, company-yasnippet-autoparens 解决的是Lisp语言里独有的问题,C语言里根本不存在我说的那个问题。