Cloel: 用Clojure扩展Emacs

用Python扩展Emacs已经非常爽了, lsp-bridge和EAF都通过外部进程和多线程RPC互调用极大的增强了Emacs的能力和性能。

最近准备开发一个Clojure + Elisp的编程框架,实现类似 python-bridge 的效果:当Elisp处理性能不够或者分析大量数据会产生GC时,通过RPC来调用Clojure函数来加速Emacs。

用Clojure的好处是, 不论是写Emacs这边的逻辑还是写外部加速代码都是Lisp风格的, 体感一样,这样贡献者会相对多一点。

当初实现EAF的动力是想在Emacs中实现多媒体界面,实现lsp-bridge的动力是现有的lsp client性能实在是太拉垮, 写代码卡一卡的没感觉。

这段时间卖公司的硬件实在太忙了, 想给自己下班找点有趣的事情做,调节一下心情, 大家有没有想增强Emacs的地方?

如果是我感兴趣的地方, 我会造一把新的 Clojure 的编程框架(“锤子”), 欢迎大家贡献“钉子”, 哈哈哈哈。

21 个赞

有一个算不上增强的点: 之前论坛里有一个 emacs-rainbow-fart, 如果能实现 Clojure 和 Emacs 之间的通信的话, 可以借助 Overtone 做合成器, 可以实现类似 mikutap 这样 “声音污染” 效果的给每个键都绑定一个触发音, (更好玩的效果估计是可以捏一个状态机来转换).

配合 holo-layer 的光标特效估计会很好玩.

又: 想问问大佬 RPC 和类似于 SLY (或者 cider) 这样的方式有那些不同呢?

2 个赞

要不要分享一下感兴趣的地方是哪里?

可以再造一个 emacs-rime 的轮子吗?

1 个赞

声音holo-layer就可以做哈

已经解决就不重复造了,狗哥的rime很好用

rpc主要可以实现两边进程代码自动化,就像lsp-bridge一样。

sly目前我感觉更像解释环境。

对接上 nrepl 就能打通大部分生态,clj, cljs, bb 都可用。 但是不知道可以用来做什么的样子。

2 个赞

Emacs 可以在远端运行server,然后在浏览器打开界面,登陆就好了。我用Rstudio server或者Python 的jupyter notebook,它们可以完全部署在server端,然后网页访问,提供了近乎一致的GUI界面。不知道Emacs是否可以这样做,从而在terminal下图片预览等无法完成事情,可以在web端做到。

1 个赞

其实我感觉只需要clojure的多线程和库生态支持,所有耗时的emacs插件代理給clojure来开发

2 个赞

大佬,可以参考一下clomacs

1 个赞

它这个要在emacs端起一个httpd的server,感觉不需要呀

1 个赞

感觉大佬说的其实是 Clojure 配合 Web 来实现一套新的 Emacs 前端, 类似 NeoVim 做的事情。

然后 Clojure 这个 Web Server 来和 Emacs Server 通讯, 让 Emacs 主要对接Elisp插件。

1 个赞

不敢在猫哥面前当大佬。 我猜测应该是这样的,不管是Clojure 还是Python 来实现。当然这样的代价,就是在web端,我们网页打开的界面,基本丢失了我们Emacs配置的快捷键设计,大部分情况下需要鼠标操作了,但用Emacs在Web端的一个主要目的不是为了一直生活在Web端,只是提供一个GUI界面来观测程序中产生的图片或者pdf文件等。

2 个赞

我和 @DogLooksGood 的想法一致。如果要用clojure扩展emacs,那最好对接nrepl。clojure作为lisp语言,相对于其他主流开发语言最大的特色恐怕就是它的repl了,整个开发流程都和repl密不可分。有了nrepl整个生态就进来了,clojure、java、python、js的的库都可以用上,并且可以用lisp的方式来写。另外,有了repl开发插件时的难度会降低很多,在eaf里面用其他语言开发插件现在debug还是比较麻烦的。

我用过一段时间clomacs,基本上能满足我的需求,就是性能上感觉有问题,搞不好就与httpd的通信方式相关。

1 个赞

Clojure启动速度太慢了,如果用CL应该启动速度上会好很多。SLIME里面已经有一个简单的RPC客户端(SWANK),SLY作为SLIME的分支应该也有一个类似的。

1 个赞

sly 的话有一个 slynk:eval-in-emacs (需要开启 sly-enable-evaluate-in-emacst), 可以做 lisp 反向调用 emacs. 一个例子就是可以让 lisp 给 emacs 发送一张图片然后显示在 mrepl 里面:

;; common lisp
(defun insert-image (path)
  (let ((path (uiop:native-namestring (truename path))))
    (slynk:eval-in-emacs
     `(sly-insert-image ,path)
     t)))
;; elisp
(defun sly-insert-image (path)
  (with-current-buffer (sly-mrepl--buffer-name sly-default-connection)
    (save-excursion
      (goto-char sly-mrepl--output-mark)
      (insert-before-markers "\n\n")
      (previous-line)
      (insert-image (create-image path)))))

效果是如下所示: (如果做得好的话估计还能像 CLIM 一样)

这种可以做成类似于 Mathematica 的 Dynamic 的效果:

  • Emacs 前端只需要做显示图片, 或者显示一个渲染的 HTML 或者 QT 组件之类的, 绑定一个可计算的实例的 uuid
  • 后端在每一次更新的时候都去找那个 uuid 对应的渲染过程然后去重新渲染对象
  • 然后做一个类似 sly-clear-mrepl 的函数去清空释放不需要的绘制对象, 防止像 Mathematica Notebook 那样绘制的元素多了把 Notebook 给卡死.

(感觉还是 holo-layer 来做这种事情会更爽… )

cl 的话缺点还是缺少一些辅助框架吧, 如果像 Python 一样有一堆实用的库可以调的话估计会轻松很多.

3 个赞

python啥库都有

主要是自己写了一些自用的代码, 不太方便迁移到 Python 里面, (其实实在不行也可以多个语言混着调用, 哪个语言某个功能做得比较好就用那个语言去实现特定的功能).

比如调用 Mathematica 的符号运算 (Mathematica 没有一个很好的宏展开, 所以用 Lisp 糊了一个翻译器给 Mathematica):

# shell create MMA server
wolframscript -code "socket = SocketConnect[\"tcp://127.0.0.1:2333\", \"ZMQ_REP\"];
While[True,
  expr = ByteArrayToString[SocketReadMessage[socket]];
  res = Check[ToExpression[expr], $Failed];
  WriteString[socket, ToString[FullForm[res]]];
]"

然后 Lisp 的简单调用形式如下:

(defun to-wolfram-name-form (symbol &optional (first-upcase nil))
  (let ((name (str:join "" (str:split "-" (format nil "~:(~a~)" symbol)))))
    (cond (first-upcase name)
	  (t
	   (str:concat (str:downcase (str:s-nth 0 name))
		       (subseq name 1))))))

(defun lisp-expr-to-wolfram-lang (expr)
  (cond ((atom expr)
	 (etypecase expr
	   (keyword (to-wolfram-name-form expr))
	   (symbol  (to-wolfram-name-form expr))
	   (integer (format nil "~d" expr))
	   (float   (format nil "~f" expr))
	   (string  (format nil "~s" expr))))
	(t
	 (let ((fn  (car expr))
	       (arg (cdr expr)))
	   (format nil "~a[~{~a~^, ~}]"
		   (case fn
		     (+ "Plus")
		     (- "Plus[#[[1]],Minus[Total@#[[2;;-1]]]]&@List")
		     (* "Times")
		     (/ "Div")
		     (expt "Power")
		     (otherwise (to-wolfram-name-form fn t)))
		   (mapcar #'lisp-expr-to-wolfram-lang arg))))))

(defmacro wolfram-eval (&body body)
  `(send-to-wolfram
    (format nil "~{~a~^;~}" (mapcar #'lisp-expr-to-wolfram-lang ',body))))

效果就是前面在 mrepl 中插入绘图的那个效果:

(wolfram-eval
      (export "~/Buff/test.png"
	      (plot (sin x) (list x -1 1))))

做好宏展开和一些绑定的话基本上根本感觉不到是在写别的语言. 类似的工作应该也有 py4cl (call python from common lisp). 不过缺点就是可能性能或者安全性上会有一些问题? 但是个人自用应该是完全没有问题的.

1 个赞

emacs 调用 lisp 的话还有两个 sly-evalsly-eval-async (这个就不会卡 Emacs), 目前我是把 emacs 当成一个 common lisp 的前端, 启动的时候连接到后台的 lisp server 上, Clojure 不清楚能不能这样做, 如果可以的话应该就不会有前面说的 Clojure 启动速度的问题.

1 个赞

我感觉从现代库的生态来讲, Python生态最好, Clojure在Web计算的生态更好? 而 Common Lisp 除了计算能力强以外, 现代一点的库生态比较差?

个人理解, 可能不对

1 个赞