Haskell的Emacs IDE伪装

两年之前撰文 Haskell的Atom IDE伪装,那时刚离开英国,因为在旁听一门大一新生的函数式编程入门课时某个契机启发了主讲教授,便委托我撰写了一个配置指南。期间我们还因为依赖的安装互相通过邮件交流了很多。现在当我再次去网站上浏览时发觉atom是仅剩的指导了,说来惭愧,我现在不怎么用atom了,使用atom取代emacs并不是我的本意,至少我并不希望任何一款editor占据主导地位。

做工具的主人,而不是奴隶

我始终觉得工具越是开箱即用越好,曾几何时,这个赞誉属于过 Macbook。在开发工具领域,很多优质的商业化集成开发环境就是好的样本,诚然这样的工具并不是针对每一种编程语言都随处可见。不同的语言需要不同的理解和优化,好的IDE需要投入巨大的成本去开发。所以很多小众的语言依然需要我们自行配置开发环境。

Emacs for Haskell

我只需要为Haskell做最基础的配置,尽量做一个简洁的环境出来,所以这里不选择「Spacemacs」。我这里打算使用的是 Aquamacs,这是一个专门为Mac改良的Emacs,当作一个特别的发行版好了。

首先,我很久没有管理我之前的配置了,这次突然又折腾一遍是因为我买了台新的「Macbook Pro 15’」,我习惯第一时间先折腾环境。

然后我开始了「吃苦」的旅程。

Brew

不知道是GFW的原因还是被M$收购的原因,不在终端配个export ALL_PROXY都连不上Github

Stack

Stack的官方软件源实在是不能用,我换了清华的源后好多了。有了Stack当然能方便不少,但是呢,老问题来了,我16年的时候开发网站就遇到过的:版本兼容。有些包还是不能在每一个resolver里面都有,我的办法是找不到的用cabal install,除此外别无他法。

Emacs

这个槽点就多了,首先在某个版本后,把什么gnutls的插件取消了,然后我不得不疯狂Goolge:自己搞了个SSL,然后配置上了。接着呢,在Melpa中很多插件在网站列表里有,但是我本地怎么都拉不到(flycheck),所幸遇到神器intero,不然我可能就要用一个简陋版本了。

完整的配置文档

先装依赖:

1
2
$ brew install gnutls libressl
$ stack install happy apply-refact intero hlint stylish-haskell hasktags hoogle

而后是完整文档,完整快捷键配置需参考注释,常用如下:

  • C-c C-l 打开repl
  • C-c h Hoogle
  • M-x speedbar 打开speedbar
  • C-c r 接受hlint光标所在位置的建议,自动重构
  • C-c b 接受hlint全局的建议,自动重构
  • M-s M-s stack mode
  • M-. 跳转到定义(需先保存文件,保存文件会自动触发tags和stylish)
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
;; stack install happy intero apply-refact hlint stylish-haskell hasktags hoogle

;; for SSL issue: https://github.com/davidswelt/aquamacs-emacs/issues/133
;; $ brew install libressl
;; $ echo $(brew --prefix)/etc/libressl/cert.pem
;; /usr/local/etc/libressl/cert.pem

;; Prerequisite - begin

(defun hlint-refactor-call-process-region-checked (start end program &optional args)
"Send text from START to END to PROGRAM with ARGS.
This is a wrapper around `call-process-region' that doesn't replace
the region with the output of PROGRAM if it returned a non-zero
exit code."
(let ((exit (apply 'call-process-region
start end
program ; name of program
t ; delete region
t ; send output to buffer
nil ; no redisplay during output
args
)))
(unless (eq exit 0) (primitive-undo 1 buffer-undo-list))))


(defun hlint-refactor-call-process-region-preserve-point (start end program &optional args)
"Send text from START to END to PROGRAM with ARGS preserving the point.
This uses `call-process-region-checked' internally."
(let ((line (line-number-at-pos))
(column (current-column)))
(hlint-refactor-call-process-region-checked start end program args)
(goto-line line)
(move-to-column column)))

;;;###autoload
(defun hlint-refactor-refactor-buffer (&optional args)
"Apply all hlint suggestions in the current buffer.
ARGS specifies additional arguments that are passed to hlint."
(interactive)
(hlint-refactor-call-process-region-preserve-point
(point-min)
(point-max)
"hlint"
(append '("--refactor"
"-")
args)))

;;;###autoload
(defun hlint-refactor-refactor-at-point ()
"Apply the hlint suggestion at point."
(interactive)
(let ((col (number-to-string (+ 1 (current-column))))
(line (number-to-string (line-number-at-pos))))
(hlint-refactor-refactor-buffer
(list (concat "--refactor-options=--pos " line "," col)))))

;;;###autoload
(define-minor-mode hlint-refactor-mode
"Automatically apply hlint suggestions"
:lighter " hlint-refactor"
:keymap (let ((map (make-sparse-keymap)))
(define-key map "\C-cb" 'hlint-refactor-refactor-buffer)
(define-key map "\C-cr" 'hlint-refactor-refactor-at-point)
map))

(provide 'hlint-refactor)

;;Pre - end

(set-frame-size (selected-frame) 140 55)

(setenv "PATH" (concat (getenv "PATH") ":/usr/local/bin/"))
(setq exec-path (append exec-path '("/usr/local/bin/")))

(with-eval-after-load 'tls
(push "/usr/local/etc/libressl/cert.pem" gnutls-trustfiles))

(global-linum-mode t)

(require 'cl)

(when (>= emacs-major-version 24)
(require 'package)
(add-to-list
'package-archives
'("melpa" . "http://melpa.org/packages/") t))

(package-initialize)
(package-refresh-contents)

;; Install Intero
(package-install 'intero)
;;(add-hook 'haskell-mode-hook 'intero-mode)

(package-install 'exec-path-from-shell)
(exec-path-from-shell-initialize)

(defvar prelude-packages
'(haskell-mode)
"A list of packages to ensure are installed at launch.")

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

(if (version<= "24.0" emacs-version)
(unless (prelude-packages-installed-p)
;; check for new packages (package versions)
(message "%s" "Emacs Prelude is now refreshing its package database...")
(package-refresh-contents)
(message "%s" " done.")
;; install the missing packages
(dolist (p prelude-packages)
(when (not (package-installed-p p))
(package-install p)))))

;; Haskell
(defun my-haskell-hook ()
(progn
(hlint-refactor-mode)
(interactive-haskell-mode)
(intero-mode)
(haskell-doc-mode)
(haskell-indentation-mode)
))

(add-hook 'haskell-mode-hook 'my-haskell-hook)

(intero-global-mode 1)

(with-eval-after-load 'intero
(flycheck-add-next-checker 'intero '(warning . haskell-hlint))
)

;; shortcut to set intero targets: Alt-s Alt-t
(global-set-key (kbd "M-s M-t") #'intero-targets)

;; install stack mode with shortcut: Alt-s Alt-s
(package-install 'hasky-stack)
(global-set-key (kbd "M-s M-s") #'hasky-stack-execute)

;; use Shift-arrow keys to move between windows
(windmove-default-keybindings)

(require 'haskell-mode)
(define-key haskell-mode-map "\C-ch" 'haskell-hoogle)
;(setq haskell-hoogle-command "hoogle")

;; use M-. on a name in a Haskell buffer which will jump directly to its definition
(setq haskell-tags-on-save t)

;; M-x speedbar
(require 'speedbar)
(speedbar-add-supported-extension ".hs")

(custom-set-variables
;; custom-set-variables was added by Custom.
;; If you edit it by hand, you could mess it up, so be careful.
;; Your init file should contain only one such instance.
;; If there is more than one, they won't work right.
'(haskell-stylish-on-save t)
'(package-selected-packages
(quote
(hasky-stack exec-path-from-shell intero haskell-mode flycheck company))))

(require 'linum)

(defvar linum-current-line 1 "Current line number.")
(defvar linum-border-width 1 "Border width for linum.")

(defface linum-current-line
`((t :inherit linum
:foreground "goldenrod"
:weight bold
))
"Face for displaying the current line number."
:group 'linum)

(defadvice linum-update (before advice-linum-update activate)
"Set the current line."
(setq linum-current-line (line-number-at-pos)
;; It's the same algorithm that linum dynamic. I only had added one
;; space in front of the first digit.
linum-border-width (number-to-string
(+ 1 (length
(number-to-string
(count-lines (point-min) (point-max))))))))

(defun linum-highlight-current-line (line-number)
"Highlight the current line number using `linum-current-line' face."
(let ((face (if (= line-number linum-current-line)
'linum-current-line
'linum)))
(propertize (format (concat "%" linum-border-width "d") line-number)
'face face)))

(setq linum-format 'linum-highlight-current-line)

总结

效果如下图所示,hlint代码建议:

hlint 与 haskell mode 配合

代码补全,有了intero可以不需要ghc-mod:

intero 的 autocomplete

有一款为Mac开发的App推荐:Haskell for Mac